HSDollyCam/HS_CamTourCommands.lsl
mita 31443b091f Add user manual for HS DollyCam and FOV extension; introduce project context for AI agents
- Created HS DollyCam HUD user manual (v1.3.0) detailing features, setup, commands, and troubleshooting.
- Added FOV extension manual outlining FOV commands, usage in playlists, and dollyzoom functionality.
- Introduced project context file for AI agents, specifying technology stack, critical implementation rules, and existing patterns.
2026-05-07 06:10:45 +02:00

967 lines
30 KiB
Plaintext

/*
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 <ms>)
- TOUR blocks: tour <total_ms> [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 "<x,y,z>")
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 <vector> 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 <idx>\n"
+ "/88 load <idx> (cut)\n"
+ "/88 moveto <idx> [ms]\n"
+ "/88 del <idx>\n"
+ "/88 list [from] [count]\n"
+ "/88 play <notecard> [gap_ms]\n"
+ "/88 stop\n"
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
+ "/88 cfg reload|dump\n"
+ "/88 show cams [N]\n"
+ "/88 hide cams\n"
+ "/88 lock on [<x,y,z>|uuid]\n"
+ "/88 lock off\n"
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
+ "/88 follow off\n"
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\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<llGetListLength(kv); ++i) {
string pair = llList2String(kv,i);
integer eq = llSubStringIndex(pair, "=");
if (eq < 1) jump next;
string k = llToLower(llGetSubString(pair, 0, eq-1));
string v = llGetSubString(pair, eq+1, -1);
say(" " + k + "=" + v);
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
@next;
}
return;
}
if (num == CE_EVT_MOVE_DONE ) {
hudShow();
return;
}
if (num == CE_EVT_STATE) {
// payload: reqId|<pos>|<focus>|<rot>
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 <total_ms> [mode] <idx1> <idx2> ... <idxN>
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 <total_ms> [mode] <idxA> <idxB> [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 <rad> (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");
}
}