1135 lines
31 KiB
Plaintext
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;
|
|
}
|
|
|
|
}
|
|
}
|