- 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.
990 lines
30 KiB
Plaintext
990 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");
|
|
}
|
|
}
|
|
gPendHoldMs = holdIn;
|
|
gPendWeight = wIn;
|
|
|
|
// NEW: pending FOV
|
|
gPendFovOn = fovOn;
|
|
gPendFovA = fovA;
|
|
gPendFovB = fovB;
|
|
|
|
if (!gCamReady) {
|
|
llMessageLinked(LINK_SET, CE_CMD_INIT, "src=TOUR", gOwner);
|
|
return;
|
|
}
|
|
|
|
if (gPendStartFirst) {
|
|
startPendingAtFirst();
|
|
return;
|
|
}
|
|
|
|
requestStateForPending();
|
|
return;
|
|
}
|
|
}
|
|
}
|