HSDollyCam/HS_CamPlaylist.lsl
2026-05-20 06:03:40 +02:00

1135 lines
31 KiB
Plaintext

/*
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 = <px, py, pz>;
gTmpFoc = <fx, fy, fz>;
// 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 <ms>"
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 <a> <b>
// fov <radA> <radB>
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> (deg 10..179)
// fov <rad> (rad ~ 0.17..3.12)
// Optional:
// fovdeg <deg> <quiet> (quiet=1 default)
// fov <rad> <quiet>
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 <ms> [mode] <idxA> <idxB>
// Builds a 2-point TOUR with FOV from the two presets.
// dollyzoom <ms> [mode] <idxA> <idxB> [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 <arg>
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|<posOff>|<focOff>|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;
}
}
}