/* HS_DollyCam - CamController (SLIM) - /88 chat parsing - Presets via Linkset Data (save/load/delete/list) - Playlist player from notecards (one command per line, optional wait ) - TOUR blocks: tour [linear|spline] ... endtour - Config reload/dump - Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message Notes: - idx is valid only if > 0 (slot 0 reserved). - Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut). - Early-cut is disabled for tours (tour is one continuous move). */ integer CH = 88; // ===== DEMO MODE ===== // If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS) integer DEMO_MODE = FALSE; integer DEMO_MAX_SLOTS = 5; // Engine protocol (must match CamEngine) integer CE_CMD_INIT = 1000; integer CE_CMD_RELEASE = 1001; integer CE_CMD_MOVE = 1010; integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride integer CE_CMD_STOP = 1012; integer CE_CMD_LOCK = 1020; integer CE_CMD_FOLLOW = 1030; integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional) integer CE_CMD_CFG_RELOAD = 1050; integer CE_CMD_CFG_DUMP = 1051; integer CE_CMD_GET_STATE = 1060; integer CE_EVT_READY = 2000; integer CE_EVT_DENIED = 2001; integer CE_EVT_MOVE_DONE = 2010; integer CE_EVT_CFG_DUMP = 2051; integer CE_EVT_STATE = 2060; // Helper scripts (separate memory budgets) integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE") integer MN_CMD = 5200; // Menu helper -> Controller integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx) // Controller -> Playlist helper integer PH_CMD_PLAY = 6100; integer PH_CMD_STOP = 6101; integer PH_CMD_CHAT_TOUR = 6102; integer PH_CMD_TOURRUN = 6103; integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands // ===== RLVa FOV ===== float RLV_FOV_MIN_DEG = 10.0; float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases // Presets string PRE_KEY(integer idx) { return "P" + (string)idx; } // Controller runtime key gOwner; integer gListen; // Move ids integer gMoveId = 100; // start non-zero integer nextMoveId() { gMoveId++; return gMoveId; } // Defaults (updated when engine cfg dump arrives) integer gDefaultMoveMs = 2200; integer demoSlotOk(integer idx) { if (!DEMO_MODE) return TRUE; if (idx <= DEMO_MAX_SLOTS) return TRUE; say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots"); return FALSE; } // ---- save pending ---- integer gSavePending = FALSE; integer gSaveIdx = 0; integer gSaveReq = 0; // Temp preset buffer (set by loadPreset) vector gTmpPos; vector gTmpFoc; integer gTmpHasFov = FALSE; float gTmpFovRad = 0.0; // “last set by HUD” bleibt als Fallback ok float gLastFovRad = 1.04719755; // ~60° // --- Marker menu state (persist across scripts) --- string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12" // ===== ADD: Follow/Lock state persisted for Menu via LinksetData ===== // FOLLOW: "on|uuid" // LOCK: "on|arg" (uuid or "") string LSKEY_FOLLOW = "HS_FOLLOW"; string LSKEY_LOCK = "HS_LOCK"; followLockInitState() { if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|"); if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|"); } followWrite(integer on, key target) { if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|"); else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target); } lockWrite(integer on, string arg) { if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|"); else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg); } // small helpers (keep controller changes localized) engineFollowOff() { // Engine expects at least "on|target" llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner); followWrite(FALSE, NULL_KEY); } engineLockOff() { llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner); lockWrite(FALSE, ""); } integer gCamsShown = FALSE; integer gCamsN = 12; camsLoadState() { string s = llLinksetDataRead(LSKEY_CAMS); if (s == "") { gCamsShown = FALSE; gCamsN = 12; llLinksetDataWrite(LSKEY_CAMS, "0|12"); return; } list p = llParseString2List(s, ["|"], []); if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0); if (llGetListLength(p) >= 2) { integer n = (integer)llList2String(p, 1); if (n < 1) n = 1; if (n > 30) n = 30; gCamsN = n; } } camsWriteState() { llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN); } float clampf(float v, float lo, float hi) { if (v < lo) return lo; if (v > hi) return hi; return v; } float clampFovRad(float rad) { float deg = rad2deg(rad); deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG); return deg2rad(deg); } string presetDescribe(integer idx, string data) { list p = llParseString2List(data, ["|"], []); integer L = llGetListLength(p); if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)"; vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>; vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>; string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc; // v2: optional fovRad at index 10 if (L >= 11) { float fr = (float)llList2String(p, 10); if (fr > 0.0001) { fr = clampFovRad(fr); float deg = rad2deg(fr); s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)"; return s; } } s += " fov=(none)"; return s; } // ===== RLVa helpers ===== float deg2rad(float deg) { return deg * PI / 180.0; } float rad2deg(float rad) { return rad * 180.0 / PI; } string fmtFloat(float v) { // LSL string(float) is fine; this is just to keep logs readable string s = (string)v; if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9); return s; } // ---------- helpers ---------- integer isValidIdx(integer idx) { return (idx > 0); } say(string s) { llOwnerSay(s); } // Playlist / TourCommand helpers phStop(string reason) { llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner); } phPlay(string card, integer gapMs) { llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner); } phChatTour(string line) { llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner); } phMenuTourRun(string raw) { llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner); } phChatDollyZoom(string line) { llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner); } // Single-prim HUD (Controller in ROOT) hudHide() { llSetAlpha(0.0, ALL_SIDES); } hudShow() { llSetAlpha(1.0, ALL_SIDES); } // ---------- engine commands ---------- engineInit() { llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner); } engineRelease() { llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner); } integer engineMove(vector pos, vector foc, integer durMs) { integer mid = nextMoveId(); string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL"; llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner); // Hide HUD during non-instant moves (avoid flicker on cuts) if (durMs > 0) hudHide(); return mid; } engineStopMove() { llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner); } engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); } engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); } // ---------- presets ---------- string packPreset(vector pos, vector foc, rotation rot, float fovRad) { // v2 preset format: + fovRad at the end (index 10) return llDumpList2String([ (string)pos.x,(string)pos.y,(string)pos.z, (string)foc.x,(string)foc.y,(string)foc.z, (string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s, (string)fovRad ], "|"); } integer loadPreset(integer idx) { string data = llLinksetDataRead(PRE_KEY(idx)); if (data == "") return FALSE; // packed: px|py|pz|fx|fy|fz|rx|ry|rz|rs (we only need first 6 here) list p = llParseString2List(data, ["|"], []); if (llGetListLength(p) < 6) return FALSE; gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>; gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>; gTmpHasFov = FALSE; gTmpFovRad = 0.0; if (llGetListLength(p) >= 11) { float fr = (float)llList2String(p, 10); if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); } } return TRUE; } applyLoadedPresetFov() { if (!gTmpHasFov) return; gLastFovRad = gTmpFovRad; // keep save fallback in sync llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner); } // ---------- follow parsing helpers ---------- integer FOLLOW_YAW = 0; integer FOLLOW_LOCAL = 1; integer FOLLOW_WORLD = 2; integer followModeFrom(string s) { s = llToLower(s); if (s == "yaw") return FOLLOW_YAW; if (s == "local") return FOLLOW_LOCAL; return FOLLOW_WORLD; } // Re-join tokens that represent a that may contain spaces. // returns [string joined, integer nextIndex] list takeAngleToken(list toks, integer i) { integer n = llGetListLength(toks); if (i >= n) return ["", i]; string s = llList2String(toks, i); if (llGetSubString(s, 0, 0) != "<") { return [s, i + 1]; } while (i + 1 < n && llGetSubString(s, -1, -1) != ">") { ++i; s += " " + llList2String(toks, i); } return [s, i + 1]; } // ---------- chat commands ---------- printHelp() { say( "HS DollyCam — Commands (/88)\n" + "/88 help\n" + "/88 cam on|off\n" + "/88 save \n" + "/88 load (cut)\n" + "/88 moveto [ms]\n" + "/88 del \n" + "/88 list [from] [count]\n" + "/88 play [gap_ms]\n" + "/88 stop\n" + "/88 tour [mode] ...\n" + "/88 cfg reload|dump\n" + "/88 show cams [N]\n" + "/88 hide cams\n" + "/88 lock on [|uuid]\n" + "/88 lock off\n" + "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n" + "/88 follow off\n" + "/88 fov (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n" + "/88 fovdeg (sets viewer FOV via RLVa; deg 10..179)\n" + "/88 dollyzoom [mode] \n" ); } // ---------- default ---------- default { state_entry() { gOwner = llGetOwner(); camsLoadState(); followLockInitState(); // NEW: ensure menu keys exist hudShow(); gListen = llListen(CH, "", "", ""); say("HS DollyCam Controller (slim) ready. Type /88 help"); engineCfgDump(); // AUTO CAM ON when script starts while worn if (llGetAttached() != 0) engineInit(); } on_rez(integer sp) { gOwner = llGetOwner(); } attach(key id) { if (id == NULL_KEY) { phStop("HUD detached."); hudShow(); gCamsShown = FALSE; camsWriteState(); // keep menu toggles consistent after detach followWrite(FALSE, NULL_KEY); lockWrite(FALSE, ""); // IMPORTANT: // Do NOT also send MC_CMD/HM_CMD here. // Markers/Manual scripts should clean up in their own attach(NULL_KEY), // otherwise you'll get double cleanup logs. // llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner); // llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner); engineRelease(); } else { gOwner = llGetOwner(); hudShow(); engineInit(); engineCfgDump(); } } link_message(integer sender, integer num, string str, key id) { if (num == CE_EVT_READY) { say("Camera control granted."); return; } if (num == CE_EVT_DENIED) { say("Camera permission denied."); return; } if (num == CE_EVT_CFG_DUMP) { list kv = llParseString2List(str, ["|"], []); integer i; say("Engine cfg:"); for (i=0; i|| list p = llParseString2List(str, ["|"], []); if (llGetListLength(p) < 4) return; integer req = (integer)llList2String(p,0); if (gSavePending && req == gSaveReq) { vector pos = (vector)llList2String(p,1); vector foc = (vector)llList2String(p,2); rotation rot = (rotation)llList2String(p,3); // fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback float fovRad = gLastFovRad; if (llGetListLength(p) >= 5) { float got = (float)llList2String(p, 4); if (got > 0.0001) fovRad = got; } fovRad = clampFovRad(fovRad); string data = packPreset(pos, foc, rot, fovRad); llLinksetDataWrite(PRE_KEY(gSaveIdx), data); // volle Anzeige: say("Saved " + presetDescribe(gSaveIdx, data)); gSavePending = FALSE; } return; } // Marker click event from HS_CamMarkers.lsl if (num == MC_EVT_CLICK) { if (id != gOwner) return; integer idx = (integer)str; if (!isValidIdx(idx)) return; if (!loadPreset(idx)) { say("Marker click: preset not found."); return; } phStop("Interrupted by marker click."); engineStopMove(); applyLoadedPresetFov(); engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs); say("Loaded via marker: " + (string)idx); return; } // Menu commands from HS_CamMenu.lsl if (num == MN_CMD) { if (id != gOwner) return; // only trust owner-routed messages list p = llParseString2List(str, ["|"], []); integer len = llGetListLength(p); if (len < 1) return; string typ = llToUpper(llList2String(p, 0)); // ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) ===== if (typ == "FOLLOW" && len >= 2) { string actF = llToUpper(llList2String(p, 1)); if (actF == "OFF") { engineFollowOff(); say("Follow OFF (menu)"); return; } if (actF == "ON" && len >= 3) { key tgtF = (key)llList2String(p, 2); if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; } // mutual exclusion engineLockOff(); integer mode = FOLLOW_WORLD; integer trans = 250; string payload = "1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR + "|" + (string)mode + "|" + (string)trans; llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner); followWrite(TRUE, tgtF); say("Follow ON -> " + llKey2Name(tgtF)); return; } return; } if (typ == "LOCK" && len >= 2) { string actL = llToUpper(llList2String(p, 1)); if (actL == "OFF") { engineLockOff(); say("Lock OFF (menu)"); return; } if (actL == "ON" && len >= 3) { key tgtL = (key)llList2String(p, 2); if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; } // mutual exclusion engineFollowOff(); llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner); lockWrite(TRUE, (string)tgtL); say("Lock ON -> " + llKey2Name(tgtL)); return; } return; } // SAVE|idx if (typ == "SAVE" && len >= 2) { integer idx = (integer)llList2String(p, 1); if (!isValidIdx(idx)) { say("idx must be > 0"); return; } // DEMO limit if (!demoSlotOk(idx)) return; phStop("Interrupted by menu save."); gSavePending = TRUE; gSaveIdx = idx; gSaveReq++; llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner); say("Saving preset " + (string)idx + " ..."); return; } // MOVETO|idx|ms if (typ == "MOVETO" && len >= 3) { integer idx2 = (integer)llList2String(p, 1); integer ms = (integer)llList2String(p, 2); if (!isValidIdx(idx2)) { say("idx must be > 0"); return; } if (!loadPreset(idx2)) { say("Preset not found."); return; } if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default phStop("Interrupted by menu moveto."); engineStopMove(); applyLoadedPresetFov(); engineMove(gTmpPos, gTmpFoc, ms); say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)"); return; } // LOAD|idx (optional: menu supports cut) if (typ == "LOAD" && len >= 2) { integer idx3 = (integer)llList2String(p, 1); if (!isValidIdx(idx3)) { say("idx must be > 0"); return; } if (!loadPreset(idx3)) { say("Preset not found."); return; } phStop("Interrupted by menu load."); engineStopMove(); applyLoadedPresetFov(); engineMove(gTmpPos, gTmpFoc, 0); say("Loaded preset " + (string)idx3 + " (cut)"); return; } // PLAY|card|gap if (typ == "PLAY" && len >= 2) { string card = llList2String(p, 1); integer gap = 0; if (len >= 3) gap = (integer)llList2String(p, 2); phPlay(card, gap); return; } // STOP if (typ == "STOP") { hudShow(); phStop("User stop."); engineStopMove(); return; } // TOURRUN|totalMs|mode|count|idx1|idx2|... if (typ == "TOURRUN") { phStop("Interrupted by menu tour."); phMenuTourRun(str); return; } // CAMS|SHOW|N or CAMS|HIDE if (typ == "CAMS" && len >= 2) { string act = llToUpper(llList2String(p, 1)); if (act == "SHOW") { integer want = gCamsN; if (len >= 3) want = (integer)llList2String(p, 2); if (want < 1) want = 1; if (want > 30) want = 30; llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner); gCamsShown = TRUE; gCamsN = want; camsWriteState(); return; } if (act == "HIDE") { llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner); gCamsShown = FALSE; camsWriteState(); return; } } return; } } listen(integer channel, string name, key id, string msg) { // Accept commands from: my avatar OR any object owned by me (including attachments) if (llGetOwnerKey(id) != gOwner) return; msg = llStringTrim(msg, STRING_TRIM); if (msg == "") return; list t = llParseString2List(msg, [" "], []); integer n = llGetListLength(t); string cmd = llToLower(llList2String(t,0)); if (cmd == "help") { printHelp(); return; } if (cmd == "cam" && n >= 2) { string sw = llToLower(llList2String(t,1)); if (sw == "on") engineInit(); else engineRelease(); hudShow(); return; } if (cmd == "cfg" && n >= 2) { string sub = llToLower(llList2String(t,1)); if (sub == "reload") engineCfgReload(); else engineCfgDump(); return; } if (cmd == "save" && n >= 2) { integer idx = (integer)llList2String(t,1); if (!isValidIdx(idx)) { say("idx must be > 0"); return; } // DEMO limit if (!demoSlotOk(idx)) return; phStop("Interrupted by save."); gSavePending = TRUE; gSaveIdx = idx; gSaveReq++; llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner); say("Saving preset " + (string)idx + " ..."); return; } if ((cmd == "del" || cmd == "delete") && n >= 2) { integer idx2 = (integer)llList2String(t,1); if (!isValidIdx(idx2)) { say("idx must be > 0"); return; } llLinksetDataDelete(PRE_KEY(idx2)); say("Deleted preset " + (string)idx2); return; } if (cmd == "load" && n >= 2) { integer idx3 = (integer)llList2String(t,1); if (!isValidIdx(idx3)) { say("idx must be > 0"); return; } if (!loadPreset(idx3)) { say("Preset not found."); return; } phStop("Interrupted by load."); engineStopMove(); applyLoadedPresetFov(); engineMove(gTmpPos, gTmpFoc, 0); say("Loaded preset " + (string)idx3 + " (cut)"); return; } if (cmd == "moveto" && n >= 2) { integer idx4 = (integer)llList2String(t,1); if (!isValidIdx(idx4)) { say("idx must be > 0"); return; } integer ms = gDefaultMoveMs; if (n >= 3) ms = (integer)llList2String(t,2); if (!loadPreset(idx4)) { say("Preset not found."); return; } phStop("Interrupted by moveto."); engineStopMove(); applyLoadedPresetFov(); engineMove(gTmpPos, gTmpFoc, ms); say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)"); return; } if (cmd == "stop") { hudShow(); phStop("User stop."); engineStopMove(); return; } if (cmd == "play" && n >= 2) { string card = llList2String(t,1); integer gap = 0; if (n >= 3) gap = (integer)llList2String(t,2); phPlay(card, gap); return; } // Chat one-liner Tour: // /88 tour [mode] ... if (cmd == "tour") { // Delegate heavy parsing/building to playlist helper (one-shot tour) phStop("Interrupted by tour (chat)."); phChatTour(msg); return; } // Chat one-liner DollyZoom: // /88 dollyzoom [mode] [keepframe? optional later] if (cmd == "dollyzoom") { phStop("Interrupted by dollyzoom (chat)."); phChatDollyZoom(msg); return; } if (cmd == "list") { integer from = 1; integer count = 20; if (n >= 2) from = (integer)llList2String(t,1); if (n >= 3) count = (integer)llList2String(t,2); if (from < 1) from = 1; integer shown = 0; integer i; for (i = from; i <= 999 && shown < count; ++i) { string data = llLinksetDataRead(PRE_KEY(i)); if (data != "") { say(presetDescribe(i, data)); shown++; } } if (!shown) say("No presets found in range."); return; } // markers routed to helper script if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") { integer want = 12; if (n >= 3) want = (integer)llList2String(t,2); if (want < 1) want = 1; if (want > 30) want = 30; llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner); gCamsShown = TRUE; gCamsN = want; camsWriteState(); return; } if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") { llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner); gCamsShown = FALSE; camsWriteState(); return; } if (cmd == "lock" && n >= 2) { string sw3 = llToLower(llList2String(t,1)); integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true"); if (!on3) { llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner); lockWrite(FALSE, ""); say("Lock OFF"); return; } // Mutual exclusion: Lock ON disables Follow engineFollowOff(); string lockArgUsed = "<0,0,0>"; if (n >= 3) { list r = takeAngleToken(t, 2); string arg = llList2String(r, 0); if (llGetSubString(arg,0,0) == "<") { lockArgUsed = arg; llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner); } else { key k = (key)arg; if (k != NULL_KEY) { lockArgUsed = (string)k; llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner); } else { vector camPos = llGetCameraPos(); rotation camRot = llGetCameraRot(); vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0); lockArgUsed = (string)foc2; llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner); } } } else { vector camPos2 = llGetCameraPos(); rotation camRot2 = llGetCameraRot(); vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0); lockArgUsed = (string)foc3; llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner); } lockWrite(TRUE, lockArgUsed); say("Lock ON"); return; } if (cmd == "follow" && n >= 2) { string sw4 = llToLower(llList2String(t,1)); integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true"); key target = gOwner; if (n >= 3) target = (key)llList2String(t,2); integer mode = FOLLOW_WORLD; integer trans = 0; if (n >= 4) mode = followModeFrom(llList2String(t,3)); if (n >= 5) trans = (integer)llList2String(t,4); if (trans < 0) trans = 0; if (!on4) { // OFF: do not touch lock llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner); followWrite(FALSE, NULL_KEY); say("Follow OFF"); return; } // Mutual exclusion: Follow ON disables Lock engineLockOff(); // capture-follow: offsets ZERO, engine captures string payload = "1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR + "|" + (string)mode + "|" + (string)trans; llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner); followWrite(TRUE, target); say("Follow ON mode=" + (string)mode + " trans=" + (string)trans); return; } // /88 fov (or degrees if >3.2) if (cmd == "fov" && n >= 2) { float v = (float)llList2String(t, 1); // Heuristic: if user typed "60", it's likely degrees float rad = v; if (v > 3.2) rad = v * PI / 180.0; rad = clampFovRad(rad); gLastFovRad = rad; // quiet=0 for manual commands, flags=1 (sync) llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner); return; } if (cmd == "fovdeg" && n >= 2) { float deg = (float)llList2String(t, 1); float rad = deg * PI / 180.0; rad = clampFovRad(rad); gLastFovRad = rad; llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner); return; } say("Unknown command. /88 help"); } } gPendHoldMs = holdIn; gPendWeight = wIn; // NEW: pending FOV gPendFovOn = fovOn; gPendFovA = fovA; gPendFovB = fovB; if (!gCamReady) { llMessageLinked(LINK_SET, CE_CMD_INIT, "src=TOUR", gOwner); return; } if (gPendStartFirst) { startPendingAtFirst(); return; } requestStateForPending(); return; } } }