/* HS_DollyCam - Playlist Helper (decoupled) - Reads playlist notecards (one command per line) - Builds TOUR payloads incl optional per-waypoint weights - Sends CE_CMD_MOVE / CE_CMD_TOUR to the core/tour engine scripts - Handles MOVE_DONE chaining + early-cut waits - Chat/menu one-shot tour builders live in HS_CamTourCommands.lsl */ // ---- Engine protocol (must match CamEngine) ---- integer CE_CMD_MOVE = 1010; integer CE_CMD_TOUR = 1011; integer CE_CMD_LOCK = 1020; integer CE_CMD_FOLLOW = 1030; integer CE_CMD_FOV = 1040; // (optional for non-tour lines later) integer CE_CMD_CFG_DUMP = 1051; integer CE_EVT_MOVE_DONE = 2010; integer CE_EVT_CFG_DUMP = 2051; // ---- Helper protocol (Controller -> Playlist Helper) ---- integer PH_CMD_PLAY = 6100; // payload: card|gapMs integer PH_CMD_STOP = 6101; // payload: reason // ---- Markers helper (optional) ---- integer MC_CMD = 5100; // "SHOW|N" / "HIDE" string LSKEY_CAMS = "HS_CAMS"; string LSKEY_LOCK = "HS_LOCK"; // ---- Presets (Linkset Data) ---- string PRE_KEY(integer idx) { return "P" + (string)idx; } // ---- Runtime ---- key gOwner; // Move ids (separate range to avoid collisions with Controller moves) integer gMoveId = 5000; integer nextMoveId() { ++gMoveId; return gMoveId; } // Defaults (updated via cfg dump) integer gDefaultMoveMs = 2200; // ---- playlist state ---- integer PL_IDLE = 0; integer PL_RUN = 1; integer PL_WAIT_MOVE = 2; integer PL_WAIT_DELAY = 3; integer gPlState = 0; integer gPlActive = FALSE; string gPlCard = ""; integer gPlLine = 0; key gPlQuery; integer gPlGapMs = 0; integer gPlSyncDepth = 0; integer gPlWaitMoveId = 0; // Prefetch / buffer integer gPlHasBuf = FALSE; string gPlBufLine = ""; integer gPlEofPending = FALSE; // Early wait after moveto integer gPlEarlyWaitActive = FALSE; float gPlResumeAt = 0.0; integer gPlEarlyCutAllowed = TRUE; // FOV integer gTourFovOn = FALSE; float gTourFovA = 0.0; // radians float gTourFovB = 0.0; // radians // ---- temp preset buffer ---- vector gTmpPos; vector gTmpFoc; // NEW: optional preset FOV (v2 format, field 10) integer gTmpHasFov = FALSE; float gTmpFovRad = 0.0; // NEW: enable/disable applying preset FOV on moveto/load (outside tours) integer APPLY_PRESET_FOV = TRUE; // ---- tour build state ---- integer gTourActive = FALSE; integer gTourTotalMs = 0; string gTourMode = "linear"; list gTourIdx; // integers; pos/focus are loaded only when the payload is built list gTourHoldMs; // integers list gTourWeight; // floats integer gTourHasWeight = FALSE; integer gTourMaxPoints = 20; // default, overridden by cfg dump integer gTourPayloadMax = 1800; // safety: keep link_message payloads reasonable // ---- HUD show/hide (single prim HUD) ---- hudHide() { llSetAlpha(0.0, ALL_SIDES); } hudShow() { llSetAlpha(1.0, ALL_SIDES); } // ---- tiny helpers ---- integer isValidIdx(integer idx) { return (idx > 0); } say(string s) { llOwnerSay(s); } stopTimer() { llSetTimerEvent(0.0); } startDelayMs(integer ms) { gPlResumeAt = llGetTime() + ((float)ms / 1000.0); llSetTimerEvent(0.05); } integer isCommentOrEmpty(string line) { line = llStringTrim(line, STRING_TRIM); if (line == "") return TRUE; if (llGetSubString(line,0,0) == "#") return TRUE; if (llGetSubString(line,0,1) == "//") return TRUE; if (llGetSubString(line,0,0) == ";") return TRUE; return FALSE; } // Minimal, list-free parse of default_move_ms from cfg dump string integer findCharFrom(string s, string ch, integer start) { integer L = llStringLength(s); integer i; for (i = start; i < L; ++i) { if (llGetSubString(s, i, i) == ch) return i; } return -1; } string lineToken(string s, integer want) { integer L = llStringLength(s); integer start = 0; integer idx = 0; while (start < L) { while (start < L && llGetSubString(s, start, start) == " ") ++start; if (start >= L) return ""; integer end = findCharFrom(s, " ", start); if (end < 0) end = L; if (idx == want) return llGetSubString(s, start, end - 1); ++idx; start = end + 1; } return ""; } integer cfgGetInt(string dump, string keyName, integer def) { integer at = llSubStringIndex(dump, keyName); if (at < 0) return def; at += llStringLength(keyName); integer end = findCharFrom(dump, "|", at); string v = ""; if (end < 0) v = llGetSubString(dump, at, -1); else v = llGetSubString(dump, at, end - 1); v = llStringTrim(v, STRING_TRIM); if (v == "") return def; return (integer)v; } // ---- Preset loading ---- 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|fovRad integer start = 0; integer end = 0; integer field = 0; float px = 0.0; float py = 0.0; float pz = 0.0; float fx = 0.0; float fy = 0.0; float fz = 0.0; float fr = 0.0; while (field <= 10) { end = findCharFrom(data, "|", start); string v = ""; if (end < 0) v = llGetSubString(data, start, -1); else v = llGetSubString(data, start, end - 1); if (field <= 5 && v == "") return FALSE; if (field == 0) px = (float)v; else if (field == 1) py = (float)v; else if (field == 2) pz = (float)v; else if (field == 3) fx = (float)v; else if (field == 4) fy = (float)v; else if (field == 5) fz = (float)v; else if (field == 10) fr = (float)v; ++field; if (end < 0) jump done; start = end + 1; } @done; if (field < 6) return FALSE; gTmpPos = ; gTmpFoc = ; // NEW: optional v2 preset: fovRad at index 10 gTmpHasFov = FALSE; gTmpFovRad = 0.0; if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); } return TRUE; } // ---- TOUR weights ---- 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 = rad * 180.0 / PI; deg = clampf(deg, 10.0, 179.0); return deg2rad(deg); } // Keep mode string small; Engine already normalizes/fallbacks. string modeSanitize(string raw) { raw = llToLower(llStringTrim(raw, STRING_TRIM)); if (raw == "") return "linear"; return raw; } float deg2rad(float deg) { return deg * PI / 180.0; } float tourWeightFromLine(string line, integer startIdx) { integer i = startIdx; while (i < 24) { string tok = lineToken(line, i); if (tok == "") return 1.0; tok = llToLower(tok); if (llSubStringIndex(tok, "w=") == 0) { float w = (float)llGetSubString(tok, 2, -1); return clampf(w, 0.10, 10.0); } if (llSubStringIndex(tok, "weight=") == 0) { float w2 = (float)llGetSubString(tok, 7, -1); return clampf(w2, 0.10, 10.0); } if (tok == "w" || tok == "weight") { string next = lineToken(line, i + 1); if (next != "") return clampf((float)next, 0.10, 10.0); } if (llSubStringIndex(tok, "spd=") == 0) { float sp = (float)llGetSubString(tok, 4, -1); if (sp > 0.0001) return clampf(1.0 / sp, 0.10, 10.0); } if (llSubStringIndex(tok, "speed=") == 0) { float sp2 = (float)llGetSubString(tok, 6, -1); if (sp2 > 0.0001) return clampf(1.0 / sp2, 0.10, 10.0); } if (tok == "spd" || tok == "speed") { string next2 = lineToken(line, i + 1); if (next2 != "") { float sp3 = (float)next2; if (sp3 > 0.0001) return clampf(1.0 / sp3, 0.10, 10.0); } } ++i; } return 1.0; } // ---- TOUR builder ---- tourReset() { gTourActive = FALSE; gTourTotalMs = 0; gTourMode = "linear"; gTourIdx = []; gTourHoldMs = []; gTourWeight = []; gTourHasWeight = FALSE; gTourFovOn = FALSE; gTourFovA = 0.0; gTourFovB = 0.0; } tourBegin(integer totalMs, string mode) { if (totalMs < 0) totalMs = 0; gTourActive = TRUE; gTourTotalMs = totalMs; gTourMode = modeSanitize(mode); gTourIdx = []; gTourHoldMs = []; gTourWeight = []; gTourHasWeight = FALSE; gTourFovOn = FALSE; gTourFovA = 0.0; gTourFovB = 0.0; } integer tourAddWaypointIdxW(integer idx, float w) { if (llGetListLength(gTourIdx) >= gTourMaxPoints) return FALSE; if (llLinksetDataRead(PRE_KEY(idx)) == "") return FALSE; w = clampf(w, 0.10, 10.0); if (llFabs(w - 1.0) > 0.001) gTourHasWeight = TRUE; gTourIdx += [idx]; gTourHoldMs += [0]; gTourWeight += [w]; return TRUE; } tourAddHold(integer ms) { if (ms < 0) ms = 0; integer n = llGetListLength(gTourHoldMs); if (n < 1) return; integer cur = llList2Integer(gTourHoldMs, n - 1); cur += ms; gTourHoldMs = llListReplaceList(gTourHoldMs, [cur], n - 1, n - 1); } string tourBuildPayload(integer moveId) { integer count = llGetListLength(gTourIdx); string body = ""; integer valid = 0; integer i; for (i = 0; i < count; ++i) { integer idx = llList2Integer(gTourIdx, i); if (!loadPreset(idx)) jump next; string part = (string)gTmpPos + "|" + (string)gTmpFoc + "|" + (string)llList2Integer(gTourHoldMs, i); if (gTourHasWeight) { float w = 1.0; if (i < llGetListLength(gTourWeight)) w = llList2Float(gTourWeight, i); part += "|" + (string)w; } if (body == "") body = part; else body += "|" + part; ++valid; @next; } if (valid < 2) return ""; string payload = (string)moveId + "|" + (string)gTourTotalMs + "|" + gTourMode + "|" + (string)valid; if (gTourFovOn) payload += "|FOV|" + (string)gTourFovA + "|" + (string)gTourFovB; return payload + "|" + body; } integer tourApplyFovFromLine(string line, integer startIdx) { string fovTok = llToLower(lineToken(line, startIdx)); if (fovTok != "fovdeg" && fovTok != "fov") return FALSE; string tokA = lineToken(line, startIdx + 1); string tokB = lineToken(line, startIdx + 2); if (tokA == "" || tokB == "") return FALSE; float a = (float)tokA; float b = (float)tokB; gTourFovOn = TRUE; if (fovTok == "fovdeg") { gTourFovA = deg2rad(clampf(a, 10.0, 179.0)); gTourFovB = deg2rad(clampf(b, 10.0, 179.0)); } else { gTourFovA = deg2rad(clampf(a * 180.0 / PI, 10.0, 179.0)); gTourFovB = deg2rad(clampf(b * 180.0 / PI, 10.0, 179.0)); } return TRUE; } integer tourRunInlineFromLine(string line, integer startIdx) { integer sawPoint = FALSE; integer bad = FALSE; integer i = startIdx; while (i < 80) { string tok = lineToken(line, i); if (tok == "") jump inline_done; string low = llToLower(tok); if (low == "fovdeg" || low == "fov") { tourApplyFovFromLine(line, i); jump inline_done; } integer idx = (integer)tok; if (!isValidIdx(idx) || (string)idx != tok) { say("Tour: bad inline token: " + tok); bad = TRUE; jump inline_done; } if (llGetListLength(gTourIdx) >= gTourMaxPoints) { say("Tour: too many points, max " + (string)gTourMaxPoints); bad = TRUE; jump inline_done; } sawPoint = TRUE; if (!tourAddWaypointIdxW(idx, 1.0)) { say("Tour: bad/missing preset: " + tok); bad = TRUE; jump inline_done; } ++i; } @inline_done; if (!sawPoint) return FALSE; if (bad || llGetListLength(gTourIdx) < 2) { if (!bad) say("Tour: needs at least 2 presets."); tourReset(); plFetchNextActionable(); return TRUE; } if (llSubStringIndex(llToLower(gTourMode), "startfirst") < 0) { gTourMode += "+startfirst"; } integer mid = nextMoveId(); string payload = tourBuildPayload(mid); if (payload == "") { say("Tour: needs at least 2 valid presets."); tourReset(); plFetchNextActionable(); return TRUE; } if (llStringLength(payload) > gTourPayloadMax) { say("Tour payload too large (" + (string)llStringLength(payload) + " chars). Reduce points or simplify values."); tourReset(); plFetchNextActionable(); return TRUE; } hudHide(); llMessageLinked(LINK_SET, CE_CMD_TOUR, payload, gOwner); tourReset(); plOnMoveIssuedEx(mid, FALSE); return TRUE; } // ---- Engine sending ---- integer engineMove(vector pos, vector foc, integer durMs) { integer mid = nextMoveId(); string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=PL"; llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner); if (durMs > 0) hudHide(); return mid; } // ---- Playlist core ---- plStop(string reason) { if (!gPlActive) return; gPlActive = FALSE; gPlState = PL_IDLE; gPlHasBuf = FALSE; gPlBufLine = ""; gPlEofPending = FALSE; gPlEarlyWaitActive = FALSE; gPlEarlyCutAllowed = TRUE; tourReset(); stopTimer(); hudShow(); say("Playlist stopped. " + reason); } plRequestLine() { if (!gPlActive) return; if (gPlSyncDepth < 12) { string data = llGetNotecardLineSync(gPlCard, gPlLine); if (data != NAK) { ++gPlSyncDepth; plHandleNotecardData(data); --gPlSyncDepth; return; } } gPlQuery = llGetNotecardLine(gPlCard, gPlLine); } plFetchNextActionable() { plRequestLine(); } plHandleNotecardData(string data) { if (!gPlActive) return; if (data == EOF) { if (gPlState == PL_WAIT_MOVE || gPlState == PL_WAIT_DELAY) { gPlEofPending = TRUE; return; } plStop("EOF reached."); return; } gPlLine++; if (isCommentOrEmpty(data)) { plFetchNextActionable(); return; } if (gPlState == PL_WAIT_MOVE || gPlState == PL_WAIT_DELAY) { plBufferOrExecuteFromWaitState(data); return; } plExecuteLine(data); } plBufferOrExecuteFromWaitState(string line) { integer wms = 0; // Early cut: after moveto, if next actionable line is "wait " if (gPlState == PL_WAIT_MOVE && !gPlEarlyWaitActive && gPlEarlyCutAllowed) { string tmp = llStringTrim(line, STRING_TRIM); if (llToLower(lineToken(tmp, 0)) == "wait") { string tok1 = lineToken(tmp, 1); if (tok1 != "") { wms = (integer)tok1; if (wms < 0) wms = 0; gPlEarlyWaitActive = TRUE; startDelayMs(wms); plFetchNextActionable(); return; } } } gPlHasBuf = TRUE; gPlBufLine = line; } plContinueFromWaitState() { if (!gPlActive) return; if (gPlHasBuf) { string line = gPlBufLine; gPlHasBuf = FALSE; gPlBufLine = ""; gPlState = PL_RUN; // execute buffered line // (LSL allows calling functions declared later) // plExecuteLine(line); plExecuteLine(line); } else { if (gPlEofPending) { plStop("EOF reached."); return; } gPlState = PL_RUN; plFetchNextActionable(); } } plOnMoveIssuedEx(integer moveId, integer allowEarlyCut) { gPlState = PL_WAIT_MOVE; gPlWaitMoveId = moveId; gPlEarlyWaitActive = FALSE; gPlEarlyCutAllowed = allowEarlyCut; plFetchNextActionable(); } plStart(string card, integer gapMs) { if (llGetInventoryType(card) != INVENTORY_NOTECARD) { say("Playlist notecard not found: " + card); return; } // stop existing if (gPlActive) plStop("Restart."); gPlActive = TRUE; gPlCard = card; gPlLine = 0; gPlGapMs = gapMs; gPlState = PL_RUN; gPlHasBuf = FALSE; gPlBufLine = ""; gPlEofPending = FALSE; gPlEarlyWaitActive = FALSE; gPlEarlyCutAllowed = TRUE; tourReset(); stopTimer(); say("Playlist start: " + card + " (gap=" + (string)gapMs + "ms)"); plFetchNextActionable(); } camsWriteState(integer shown, integer n) { if (n < 1) n = 1; if (n > 30) n = 30; llLinksetDataWrite(LSKEY_CAMS, (string)shown + "|" + (string)n); } plExecuteLine(string line) { if (!gPlActive) return; line = llStringTrim(line, STRING_TRIM); if (isCommentOrEmpty(line)) { plFetchNextActionable(); return; } string cmd = llToLower(lineToken(line, 0)); if (cmd == "") { plFetchNextActionable(); return; } // ---- TOUR BLOCK ---- if (gTourActive) { if (cmd == "endtour") { integer count = llGetListLength(gTourIdx); if (count < 2) { tourReset(); plFetchNextActionable(); return; } integer mid = nextMoveId(); string payload = tourBuildPayload(mid); if (payload == "") { say("Tour: needs at least 2 valid presets."); tourReset(); plFetchNextActionable(); return; } if (llStringLength(payload) > gTourPayloadMax) { say("Tour payload too large (" + (string)llStringLength(payload) + " chars). Reduce points or simplify values."); tourReset(); plFetchNextActionable(); return; } hudHide(); llMessageLinked(LINK_SET, CE_CMD_TOUR, payload, gOwner); tourReset(); plOnMoveIssuedEx(mid, FALSE); // no early-cut for tours return; } if (cmd == "wait") { string tok1 = lineToken(line, 1); if (tok1 == "") { plFetchNextActionable(); return; } integer ms = (integer)tok1; if (ms < 0) ms = 0; tourAddHold(ms); plFetchNextActionable(); return; } if (cmd == "moveto" || cmd == "goto" || cmd == "load") { string tok1m = lineToken(line, 1); if (tok1m == "") { plFetchNextActionable(); return; } integer idx = (integer)tok1m; if (!isValidIdx(idx)) { plFetchNextActionable(); return; } float w = tourWeightFromLine(line, 2); if (!tourAddWaypointIdxW(idx, w)) { // ignore if too many or missing preset } plFetchNextActionable(); return; } // Allow FOV ramp lines inside tour blocks: // fovdeg // fov if (cmd == "fovdeg" || cmd == "fov") { tourApplyFovFromLine(line, 0); plFetchNextActionable(); return; } // ignore other commands inside tour plFetchNextActionable(); return; } // ---- NORMAL PLAYLIST ---- if (cmd == "tour") { string tok1 = lineToken(line, 1); if (tok1 == "") { plFetchNextActionable(); return; } integer totalMs = (integer)tok1; string mode = "linear"; integer scanFrom = 2; string tok2 = lineToken(line, 2); if (tok2 != "") { string tok2l = llToLower(tok2); integer maybeIdx = (integer)tok2; if (tok2l != "fovdeg" && tok2l != "fov" && (maybeIdx <= 0 || (string)maybeIdx != tok2)) { mode = tok2; scanFrom = 3; } } tourBegin(totalMs, mode); // Header FOV keeps block syntax working: // tour 9000 spline fovdeg 35 70 tourApplyFovFromLine(line, scanFrom); // Compact notecard syntax avoids waiting for many dataserver lines: // tour 9000 spline 1 2 3 4 if (tourRunInlineFromLine(line, scanFrom)) return; plFetchNextActionable(); return; } if (cmd == "wait") { string tok1w = lineToken(line, 1); if (tok1w == "") { plFetchNextActionable(); return; } integer ms = (integer)tok1w; if (ms < 0) ms = 0; gPlState = PL_WAIT_DELAY; startDelayMs(ms); plFetchNextActionable(); return; } // ---- FOV (standalone, outside tours) ---- // Usage in notecard: // fovdeg (deg 10..179) // fov (rad ~ 0.17..3.12) // Optional: // fovdeg (quiet=1 default) // fov if (cmd == "fovdeg" || cmd == "fov") { string tok1f2 = lineToken(line, 1); if (tok1f2 == "") { plFetchNextActionable(); return; } integer quiet = 1; string tok2f2 = lineToken(line, 2); if (tok2f2 != "") quiet = (integer)tok2f2; float rad = 1.0472; // fallback ~60deg if (cmd == "fovdeg") { float deg = (float)tok1f2; deg = clampf(deg, 10.0, 179.0); rad = deg2rad(deg); } else { float inrad = (float)tok1f2; // clamp via deg equivalent for sanity float indeg = clampf(inrad * 180.0 / PI, 10.0, 179.0); rad = deg2rad(indeg); } // Send to Controller -> RLVa (quiet to avoid chat spam) llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|" + (string)quiet, gOwner); // keep playlist pacing consistent with other commands if (gPlGapMs > 0) { gPlState = PL_WAIT_DELAY; startDelayMs(gPlGapMs); plFetchNextActionable(); return; } plFetchNextActionable(); return; } if (cmd == "moveto" || cmd == "goto" || cmd == "load") { string tok1m2 = lineToken(line, 1); if (tok1m2 == "") { plFetchNextActionable(); return; } integer idx = (integer)tok1m2; if (!isValidIdx(idx)) { plFetchNextActionable(); return; } if (!loadPreset(idx)) { plFetchNextActionable(); return; } // NEW: apply preset FOV (outside tours) if (APPLY_PRESET_FOV && gTmpHasFov) { llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner); } integer dur = gDefaultMoveMs; if (cmd == "load") dur = 0; string tok2m2 = lineToken(line, 2); if (tok2m2 != "") dur = (integer)tok2m2; integer mid = engineMove(gTmpPos, gTmpFoc, dur); plOnMoveIssuedEx(mid, TRUE); return; } // dollyzoom [mode] // Builds a 2-point TOUR with FOV from the two presets. // dollyzoom [mode] [keep] // Builds a 2-point TOUR with FOV from the two presets. if (cmd == "dollyzoom") { string tok1d = lineToken(line, 1); string tok2d = lineToken(line, 2); if (tok1d == "" || tok2d == "") { plFetchNextActionable(); return; } integer totalMs = (integer)tok1d; if (totalMs < 0) totalMs = 0; integer i = 2; string mode = "linear"; integer maybeIdx = (integer)tok2d; if (maybeIdx <= 0 || (string)maybeIdx != tok2d) { mode = tok2d; i = 3; } mode = modeSanitize(mode); string tokIdxA = lineToken(line, i); string tokIdxB = lineToken(line, i + 1); if (tokIdxA == "" || tokIdxB == "") { plFetchNextActionable(); return; } integer idxA = (integer)tokIdxA; integer idxB = (integer)tokIdxB; // Optional keep token after idxB integer keepOn = FALSE; integer k; for (k = i + 2; k < i + 8; ++k) { string tokK = llToLower(lineToken(line, k)); if (tokK == "") jump doneDollyOpts; if (tokK == "keep" || tokK == "keepframe" || tokK == "kf") { keepOn = TRUE; } } @doneDollyOpts; if (keepOn) mode = mode + "+keep"; if (!isValidIdx(idxA) || !isValidIdx(idxB)) { plFetchNextActionable(); return; } if (!loadPreset(idxA)) { say("DollyZoom: missing preset " + (string)idxA); plFetchNextActionable(); return; } vector posA = gTmpPos; vector focA = gTmpFoc; integer hasFovA = gTmpHasFov; float fovA = gTmpFovRad; if (!loadPreset(idxB)) { say("DollyZoom: missing preset " + (string)idxB); plFetchNextActionable(); return; } vector posB = gTmpPos; vector focB = gTmpFoc; integer hasFovB = gTmpHasFov; float fovB = gTmpFovRad; if (keepOn) { focB = focA; } // For keepframe we only need A's FOV (start FOV); B is optional. if (!hasFovA || (!keepOn && !hasFovB)) { say("DollyZoom needs FOV stored in BOTH presets (unless using 'keep'). Set FOV and save presets again."); plFetchNextActionable(); return; } if (keepOn && !hasFovB) fovB = fovA; // harmless placeholder integer mid = nextMoveId(); string payload = (string)mid + "|" + (string)totalMs + "|" + mode + "|2" + "|FOV|" + (string)fovA + "|" + (string)fovB + "|" + (string)posA + "|" + (string)focA + "|0" + "|" + (string)posB + "|" + (string)focB + "|0"; hudHide(); llMessageLinked(LINK_SET, CE_CMD_TOUR, payload, gOwner); // Tours are one continuous move: no early-cut plOnMoveIssuedEx(mid, FALSE); return; } // lock on|off if (cmd == "lock") { string tok1l = lineToken(line, 1); if (tok1l == "") { plFetchNextActionable(); return; } integer on = (llToLower(tok1l) == "on"); string arg = "<0,0,0>"; string tok2l = lineToken(line, 2); if (tok2l != "") arg = tok2l; llMessageLinked(LINK_SET, CE_CMD_LOCK, (string)on + "|" + arg, gOwner); if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|"); else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg); if (gPlGapMs > 0) { gPlState = PL_WAIT_DELAY; startDelayMs(gPlGapMs); plFetchNextActionable(); return; } plFetchNextActionable(); return; } // follow on|off ... if (cmd == "follow") { string tok1fo = lineToken(line, 1); if (tok1fo == "") { plFetchNextActionable(); return; } integer on2 = (llToLower(tok1fo) == "on"); // pass-through minimal payload: on|target|||mode|trans // For notecards: keep it simple; advanced offsets can still be written as single tokens (no spaces) key target = gOwner; string tok2fo = lineToken(line, 2); if (tok2fo != "") target = (key)tok2fo; integer mode = 2; // world integer trans = 0; string tok3fo = lineToken(line, 3); string tok4fo = lineToken(line, 4); if (tok3fo != "") mode = (integer)tok3fo; if (tok4fo != "") trans = (integer)tok4fo; string payload = (string)on2 + "|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR + "|" + (string)mode + "|" + (string)trans; llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner); if (gPlGapMs > 0) { gPlState = PL_WAIT_DELAY; startDelayMs(gPlGapMs); plFetchNextActionable(); return; } plFetchNextActionable(); return; } // markers show/hide cams if (cmd == "show" && llToLower(lineToken(line, 1)) == "cams") { integer want = 12; string tok2s = lineToken(line, 2); if (tok2s != "") want = (integer)tok2s; if (want < 1) want = 1; if (want > 30) want = 30; llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner); camsWriteState(TRUE, want); if (gPlGapMs > 0) { gPlState = PL_WAIT_DELAY; startDelayMs(gPlGapMs); plFetchNextActionable(); return; } plFetchNextActionable(); return; } if (cmd == "hide" && llToLower(lineToken(line, 1)) == "cams") { llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner); camsWriteState(FALSE, 12); if (gPlGapMs > 0) { gPlState = PL_WAIT_DELAY; startDelayMs(gPlGapMs); plFetchNextActionable(); return; } plFetchNextActionable(); return; } // Unknown: ignore to keep playlists robust plFetchNextActionable(); } // ---- Events ---- default { state_entry() { gOwner = llGetOwner(); // ask engine for cfg dump once (helper parses only default_move_ms) llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); } on_rez(integer sp) { gOwner = llGetOwner(); } attach(key id) { gOwner = llGetOwner(); if (id == NULL_KEY) { if (gPlActive) plStop("HUD detached."); } } timer() { float now = llGetTime(); if (gPlActive && gPlState == PL_WAIT_MOVE && gPlEarlyWaitActive) { if (now >= gPlResumeAt) { gPlEarlyWaitActive = FALSE; stopTimer(); plContinueFromWaitState(); } return; } if (gPlActive && gPlState == PL_WAIT_DELAY) { if (now >= gPlResumeAt) { stopTimer(); plContinueFromWaitState(); } } } dataserver(key qid, string data) { if (qid != gPlQuery) return; if (!gPlActive) return; plHandleNotecardData(data); } link_message(integer sender, integer num, string str, key id) { // Engine events if (num == CE_EVT_CFG_DUMP) { gDefaultMoveMs = cfgGetInt(str, "default_move_ms=", gDefaultMoveMs); integer n = cfgGetInt(str, "tour_max_points=", gTourMaxPoints); if (n < 2) n = 2; if (n > 40) n = 40; // safety cap gTourMaxPoints = n; return; } if (num == CE_EVT_MOVE_DONE) { // Always safe to show HUD when engine finishes a move hudShow(); if (!gPlActive) return; integer mid = (integer)str; if (gPlState == PL_WAIT_MOVE && !gPlEarlyWaitActive && mid == gPlWaitMoveId) { if (gPlGapMs > 0) { gPlState = PL_WAIT_DELAY; startDelayMs(gPlGapMs); return; } plContinueFromWaitState(); } return; } // Controller -> Helper commands (trust only owner-routed) if (id != gOwner) return; if (num == PH_CMD_STOP) { if (gPlActive) plStop(str); return; } if (num == PH_CMD_PLAY) { integer gap = 0; string card = str; integer sep = llSubStringIndex(str, "|"); if (sep >= 0) { card = llGetSubString(str, 0, sep - 1); gap = (integer)llGetSubString(str, sep + 1, -1); } if (card == "") return; plStart(card, gap); return; } } }