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

1089 lines
29 KiB
Plaintext

/*
HS_DollyCam - Menu Helper (HS_CamMenu.lsl) [UX STABLE NAV]
- Touch HUD -> llDialog menus
- Stable pagination UX:
* Always 12 buttons
* Always shows: "<<", "Back", ">>" side-by-side
* Blank filler buttons (" ") keep layout stable
* Wrap-around: << on first page -> last, >> on last -> first
* Back goes to previous menu level (not always main)
- Save presets (1..N)
- MoveTo presets (1..N) using configurable Duration
- Play playlists: notecards starting with "shot_"
- Settings: Mode linear/spline + Duration (ms)
- Tour Builder: collect points -> run one tour
Talks to Controller via:
llMessageLinked(LINK_SET, MN_CMD, "TYPE|...", owner)
Put this script into the prim(s) you want clickable.
*/
integer MN_CMD = 5200;
string NC_PREFIX = "shot_"; // play menu shows only notecards starting with this (case-insensitive)
integer PRESET_MAX = 30; // max preset slots shown in menus
// Stable 12-button layouts (no "Close"; viewer has Ignore)
integer SAVE_PER_PAGE = 9; // 9 numbers + nav(3) = 12
integer MOVE_PER_PAGE = 8; // 8 numbers + toggle(1) + nav(3) = 12
integer PLAY_PER_PAGE = 9; // 9 cards + nav(3) = 12
integer TOUR_PER_PAGE = 9; // 9 numbers + nav(3) = 12
// menu states
integer M_MAIN = 0;
integer M_SAVE = 1;
integer M_MOVETO = 2;
integer M_PLAY = 3;
integer M_SETTINGS = 4;
integer M_SETMODE = 5;
integer M_SETDUR = 6;
integer M_TOUR = 7;
integer M_TOUR_PICK = 8;
// ===== ADD: Follow/Lock Target Pick =====
integer M_FOLLOW_PICK = 9;
integer M_LOCK_PICK = 10;
// Nearby config (leicht änderbar)
float NEARBY_RANGE = 20.0; // meters
integer AV_PER_PAGE = 9; // 9 entries + navRow(3) = 12 buttons stable
// LinksetData keys written by Controller (see Controller patch)
string LSKEY_FOLLOW = "HS_FOLLOW"; // "on|uuid"
string LSKEY_LOCK = "HS_LOCK"; // "on|arg" (uuid or "<x,y,z>")
// runtime state (menu-side)
integer gFollowOn = FALSE;
string gFollowArg = ""; // uuid (string) if ON
integer gLockOn = FALSE;
string gLockArg = ""; // uuid or "<...>" if ON
// cached nearby lists for pick menus (kept across pages)
list gNearKeys; // keys
list gNearNames; // names
list gNearLabels; // short labels shown in llDialog
key gOwner;
integer gChan = 0;
integer gListen = 0;
// current menu state
integer gState = M_MAIN;
integer gPage = 0;
// previous menu stack: [state, page, state, page, ...]
list gBackStack;
// settings
integer gDurMs = 3000; // used for MoveTo + Tour total
string gMode = "linear"; // "linear" or "spline"
integer gGapMs = 0; // optional for Play (kept simple)
// MoveTo toggle
integer gCutMode = FALSE; // FALSE -> moveto, TRUE -> cut(load)
// Tour builder
list gTourIdx; // strings (idx)
// timeout
float MENU_TIMEOUT = 15.0;
// --- Marker toggle state (persisted in Linkset Data by Controller) ---
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
integer gCamsShown = FALSE;
integer gCamsN = 12;
camsLoadState()
{
string s = llLinksetDataRead(LSKEY_CAMS);
if (s == "") { gCamsShown = FALSE; gCamsN = 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;
}
}
sendCamsShow()
{
llMessageLinked(LINK_SET, MN_CMD, "CAMS|SHOW|" + (string)gCamsN, gOwner);
}
sendCamsHide()
{
llMessageLinked(LINK_SET, MN_CMD, "CAMS|HIDE", gOwner);
}
// ---- small utils ----
say(string s) { llOwnerSay(s); }
integer isOwner(key k) { return (k == gOwner); }
integer startsWithNoCase(string s, string pref)
{
s = llToLower(s);
pref = llToLower(pref);
return (llGetSubString(s, 0, llStringLength(pref)-1) == pref);
}
// ===== ADD: Follow/Lock state from Controller (LinksetData) =====
followLockLoadState()
{
// FOLLOW: "on|uuid"
gFollowOn = FALSE;
gFollowArg = "";
string fs = llLinksetDataRead(LSKEY_FOLLOW);
if (fs != "") {
list p = llParseString2List(fs, ["|"], []);
if (llGetListLength(p) >= 1) gFollowOn = (integer)llList2String(p, 0);
if (llGetListLength(p) >= 2) gFollowArg = llList2String(p, 1);
}
// LOCK: "on|arg" (uuid or "<vec>")
gLockOn = FALSE;
gLockArg = "";
string ls = llLinksetDataRead(LSKEY_LOCK);
if (ls != "") {
list q = llParseString2List(ls, ["|"], []);
if (llGetListLength(q) >= 1) gLockOn = (integer)llList2String(q, 0);
if (llGetListLength(q) >= 2) gLockArg = llList2String(q, 1);
}
}
string shortNameFromArg(string arg)
{
if (arg == "") return "";
if (llGetSubString(arg, 0, 0) == "<") return "vec";
string nm = llKey2Name((key)arg);
if (nm != "") return nm;
return llGetSubString(arg, 0, 7);
}
string followLockStatusLine()
{
string f = "OFF";
if (gFollowOn) f = "ON (" + shortNameFromArg(gFollowArg) + ")";
string l = "OFF";
if (gLockOn) l = "ON (" + shortNameFromArg(gLockArg) + ")";
return "Follow: " + f + "\nLock: " + l;
}
// ===== ADD: Nearby list building (AGENT_LIST_REGION + distance filter) =====
string sanitizeName(string nm)
{
if (nm == "") nm = "(unknown)";
// avoid breaking our label format
if (llSubStringIndex(nm, "|") != -1)
nm = llDumpList2String(llParseString2List(nm, ["|"], []), ":");
return nm;
}
string makeAgentLabel(string name, key agentId)
{
// llDialog button limit -> short + uuid prefix
string pref = llGetSubString((string)agentId, 0, 3); // 4 chars
integer maxName = 24 - (1 + 4); // "name|abcd" => name max 19 chars
if (llStringLength(name) > maxName) name = llGetSubString(name, 0, maxName - 1);
string lbl = name + "|" + pref;
// avoid reserved nav labels (paranoia)
if (lbl == "Back" || lbl == "<<" || lbl == ">>") lbl = name + "_" + pref;
return lbl;
}
buildNearbyLists()
{
gNearKeys = [];
gNearNames = [];
gNearLabels = [];
// Owner position for distance filter
vector myPos = ZERO_VECTOR;
list od = llGetObjectDetails(gOwner, [OBJECT_POS]);
if (llGetListLength(od) >= 1) myPos = llList2Vector(od, 0);
list agents = llGetAgentList(AGENT_LIST_REGION, []);
// Ensure owner included
if (llListFindList(agents, [gOwner]) == -1) agents += [gOwner];
// build sortable stride list: [nameLower, uuidStr, name, label]...
list tmp = [];
integer i;
for (i = 0; i < llGetListLength(agents); ++i)
{
key k = llList2Key(agents, i);
list d = llGetObjectDetails(k, [OBJECT_POS]);
if (llGetListLength(d) < 1) jump nextAgent;
vector p = llList2Vector(d, 0);
// distance filter; if owner pos is unavailable, include region agents
if (myPos != ZERO_VECTOR)
{
if (llVecDist(p, myPos) > NEARBY_RANGE) jump nextAgent;
}
// else owner pos unknown; keep all region agents
string nm = sanitizeName(llKey2Name(k));
string lbl = makeAgentLabel(nm, k);
tmp += [llToLower(nm), (string)k, nm, lbl];
@nextAgent;
}
if (llGetListLength(tmp) > 0)
tmp = llListSort(tmp, 4, TRUE);
for (i = 0; i < llGetListLength(tmp); i += 4)
{
gNearKeys += [(key)llList2String(tmp, i + 1)];
gNearNames += [llList2String(tmp, i + 2)];
gNearLabels += [llList2String(tmp, i + 3)];
}
}
menuClose()
{
if (gListen) llListenRemove(gListen);
gListen = 0;
llSetTimerEvent(0.0);
}
menuOpenListen()
{
menuClose();
gChan = (integer)(llFrand(1000000.0) + 1000.0) * -1;
gListen = llListen(gChan, "", gOwner, "");
llSetTimerEvent(MENU_TIMEOUT);
}
pushBack(integer st, integer pg)
{
gBackStack += [st, pg];
}
integer popBack()
{
integer n = llGetListLength(gBackStack);
if (n < 2) return FALSE;
integer prevPage = llList2Integer(gBackStack, n - 1);
integer prevState = llList2Integer(gBackStack, n - 2);
gBackStack = llDeleteSubList(gBackStack, n - 2, n - 1);
gState = prevState;
gPage = prevPage;
return TRUE;
}
// Returns a list padded with filler " " to exactly wantLen entries.
list padTo(list items, integer wantLen)
{
integer n = llGetListLength(items);
while (n < wantLen) {
items += [" "];
++n;
}
if (n > wantLen) items = llList2List(items, 0, wantLen - 1);
return items;
}
// Stable nav row: always present, always same order.
list navRow()
{
return ["<<", "Back", ">>"];
}
// Wrap-around paging
integer wrapPrev(integer page, integer pages)
{
page -= 1;
if (page < 0) page = pages - 1;
return page;
}
integer wrapNext(integer page, integer pages)
{
page += 1;
if (page >= pages) page = 0;
return page;
}
// ---- inventory listing ----
list getShotNotecards()
{
list out = [];
integer n = llGetInventoryNumber(INVENTORY_NOTECARD);
integer i;
for (i = 0; i < n; ++i) {
string name = llGetInventoryName(INVENTORY_NOTECARD, i);
if (startsWithNoCase(name, NC_PREFIX)) out += [name];
}
return out;
}
// ---- controller triggers ----
sendSave(integer idx)
{
llMessageLinked(LINK_SET, MN_CMD, "SAVE|" + (string)idx, gOwner);
}
sendMoveTo(integer idx)
{
llMessageLinked(LINK_SET, MN_CMD, "MOVETO|" + (string)idx + "|" + (string)gDurMs, gOwner);
}
sendLoad(integer idx)
{
llMessageLinked(LINK_SET, MN_CMD, "LOAD|" + (string)idx, gOwner);
}
sendPlay(string card)
{
llMessageLinked(LINK_SET, MN_CMD, "PLAY|" + card + "|" + (string)gGapMs, gOwner);
}
sendStop()
{
llMessageLinked(LINK_SET, MN_CMD, "STOP", gOwner);
}
sendTourRun()
{
integer n = llGetListLength(gTourIdx);
if (n < 2) {
say("Tour Builder: add at least 2 points.");
return;
}
// TOURRUN|totalMs|mode|count|idx1|idx2|...
list parts = ["TOURRUN", (string)gDurMs, gMode, (string)n];
integer i;
for (i = 0; i < n; ++i) parts += [llList2String(gTourIdx, i)];
llMessageLinked(LINK_SET, MN_CMD, llDumpList2String(parts, "|"), gOwner);
}
// ---- UI strings ----
string settingsLine()
{
return "Mode: " + llToUpper(gMode) + "\nDuration: " + (string)gDurMs + " ms";
}
string tourListLine()
{
integer n = llGetListLength(gTourIdx);
if (n < 1) return "(none)";
string s = "";
integer i;
for (i = 0; i < n; ++i) {
s += llList2String(gTourIdx, i);
if (i < n - 1) s += ", ";
if (llStringLength(s) > 200) { s += " ..."; jump done; }
}
@done;
return s;
}
// ---- show menus ----
showMainEx(integer doLoad)
{
camsLoadState();
if (doLoad) followLockLoadState(); // nur wenn wir "frisch" aus LSD lesen wollen
gState = M_MAIN;
gPage = 0;
gCutMode = FALSE;
gBackStack = []; // root
menuOpenListen();
string camsBtn = "Show Cams";
if (gCamsShown) camsBtn = "Hide Cams";
string msg =
"HS DollyCam Menu\n\n"
+ settingsLine()
+ "\n\n"
+ followLockStatusLine()
+ "\n\nSelect:";
list buttons = ["Follow","Lock", "Settings", "Tour", camsBtn, "Stop", "Save","MoveTo","Play"];
llDialog(gOwner, msg, buttons, gChan);
}
showMain()
{
showMainEx(TRUE);
}
showSave(integer page)
{
pushBack(gState, gPage);
gState = M_SAVE;
gPage = page;
gCutMode = FALSE;
menuOpenListen();
integer pages = (PRESET_MAX + SAVE_PER_PAGE - 1) / SAVE_PER_PAGE;
if (pages < 1) pages = 1;
if (gPage < 0) gPage = 0;
if (gPage >= pages) gPage = pages - 1;
integer start = gPage * SAVE_PER_PAGE + 1;
integer end = start + SAVE_PER_PAGE - 1;
if (end > PRESET_MAX) end = PRESET_MAX;
list btn = [];
integer i;
for (i = start; i <= end; ++i) btn += [(string)i];
btn = padTo(btn, SAVE_PER_PAGE);
btn += navRow(); // total 12
string msg =
"Save preset:\n"
+ "(Select a slot)\n\n"
+ "Page " + (string)(gPage + 1) + "/" + (string)pages
+ "\nRange: " + (string)start + "-" + (string)end;
llDialog(gOwner, msg, btn, gChan);
}
showMoveTo(integer page)
{
pushBack(gState, gPage);
gState = M_MOVETO;
gPage = page;
menuOpenListen();
integer pages = (PRESET_MAX + MOVE_PER_PAGE - 1) / MOVE_PER_PAGE;
if (pages < 1) pages = 1;
if (gPage < 0) gPage = 0;
if (gPage >= pages) gPage = pages - 1;
integer start = gPage * MOVE_PER_PAGE + 1;
integer end = start + MOVE_PER_PAGE - 1;
if (end > PRESET_MAX) end = PRESET_MAX;
list btn = [];
integer i;
for (i = start; i <= end; ++i) btn += [(string)i];
btn = padTo(btn, MOVE_PER_PAGE);
// Toggle button (always present, stable position)
if (gCutMode) btn += ["Move"];
else btn += ["Cut"];
btn += navRow(); // total 12
string modeLine;
if (gCutMode) modeLine = "Selection action: CUT (load)";
else modeLine = "Selection action: MOVETO";
string msg =
"MoveTo preset:\n"
+ "Duration: " + (string)gDurMs + " ms\n"
+ modeLine + "\n\n"
+ "Page " + (string)(gPage + 1) + "/" + (string)pages
+ "\nRange: " + (string)start + "-" + (string)end;
llDialog(gOwner, msg, btn, gChan);
}
showPlay(integer page)
{
pushBack(gState, gPage);
gState = M_PLAY;
gPage = page;
gCutMode = FALSE;
menuOpenListen();
list cards = getShotNotecards();
integer total = llGetListLength(cards);
if (total < 1) {
// Keep stable 12 buttons: 9 blanks + nav
list btn = padTo([], PLAY_PER_PAGE);
btn += navRow();
string msg =
"Play playlist:\n"
+ "No notecards found with prefix '" + NC_PREFIX + "'.\n\n"
+ "Press Back to return.";
llDialog(gOwner, msg, btn, gChan);
return;
}
integer pages = (total + PLAY_PER_PAGE - 1) / PLAY_PER_PAGE;
if (pages < 1) pages = 1;
if (gPage < 0) gPage = 0;
if (gPage >= pages) gPage = pages - 1;
integer start = gPage * PLAY_PER_PAGE;
integer end = start + PLAY_PER_PAGE - 1;
if (end > total - 1) end = total - 1;
list btn2 = [];
integer i;
for (i = start; i <= end; ++i) btn2 += [llList2String(cards, i)];
btn2 = padTo(btn2, PLAY_PER_PAGE);
btn2 += navRow();
string msg2 =
"Play playlist:\n"
+ "(Notecards starting with '" + NC_PREFIX + "')\n"
+ "gap: " + (string)gGapMs + " ms\n\n"
+ "Page " + (string)(gPage + 1) + "/" + (string)pages;
llDialog(gOwner, msg2, btn2, gChan);
}
showSettings()
{
pushBack(gState, gPage);
gState = M_SETTINGS;
gPage = 0;
gCutMode = FALSE;
menuOpenListen();
string msg =
"Settings\n\n"
+ settingsLine()
+ "\n\nSelect:";
list btn = ["Mode","Duration","Back"];
llDialog(gOwner, msg, btn, gChan);
}
showSetMode()
{
pushBack(gState, gPage);
gState = M_SETMODE;
gPage = 0;
gCutMode = FALSE;
menuOpenListen();
string msg =
"Set Tour Mode\n\n"
+ "Current: " + llToUpper(gMode) + "\n"
+ "(Used for Tours)\n";
// Keep it simple: one screen, no pagination needed
list btn = ["LINEAR","SPLINE","EASE_IN","EASE_OUT","EASE_IN_OUT","Back"];
llDialog(gOwner, msg, btn, gChan);
}
showSetDur()
{
pushBack(gState, gPage);
gState = M_SETDUR;
gPage = 0;
gCutMode = FALSE;
menuOpenListen();
string msg =
"Set Duration (ms)\n\n"
+ "Current: " + (string)gDurMs + "\n"
+ "Used for MoveTo + Tour total.\n";
list btn = ["1500","2500","3000","4000","6000","Custom","Back"];
llDialog(gOwner, msg, btn, gChan);
}
showTour()
{
pushBack(gState, gPage);
gState = M_TOUR;
gPage = 0;
gCutMode = FALSE;
menuOpenListen();
string msg =
"Tour Builder\n\n"
+ "Mode: " + llToUpper(gMode) + "\n"
+ "Total: " + (string)gDurMs + " ms\n"
+ "Points: " + tourListLine()
+ "\n\nSelect:";
list btn = ["Add","Run","Clear","Back"];
llDialog(gOwner, msg, btn, gChan);
}
showTourPick(integer page)
{
pushBack(gState, gPage);
gState = M_TOUR_PICK;
gPage = page;
gCutMode = FALSE;
menuOpenListen();
integer pages = (PRESET_MAX + TOUR_PER_PAGE - 1) / TOUR_PER_PAGE;
if (pages < 1) pages = 1;
if (gPage < 0) gPage = 0;
if (gPage >= pages) gPage = pages - 1;
integer start = gPage * TOUR_PER_PAGE + 1;
integer end = start + TOUR_PER_PAGE - 1;
if (end > PRESET_MAX) end = PRESET_MAX;
list btn = [];
integer i;
for (i = start; i <= end; ++i) btn += [(string)i];
btn = padTo(btn, TOUR_PER_PAGE);
btn += navRow();
string msg =
"Add Tour Point\n\n"
+ "Select preset index to add.\n"
+ "Current points: " + tourListLine()
+ "\n\n"
+ "Page " + (string)(gPage + 1) + "/" + (string)pages
+ "\nRange: " + (string)start + "-" + (string)end;
llDialog(gOwner, msg, btn, gChan);
}
// ===== ADD: Follow/Lock pick menus =====
showFollowPickInternal(integer page, integer rebuild)
{
pushBack(gState, gPage);
gState = M_FOLLOW_PICK;
gPage = page;
gCutMode = FALSE;
menuOpenListen();
if (rebuild) buildNearbyLists();
integer total = llGetListLength(gNearLabels);
integer pages = (total + AV_PER_PAGE - 1) / AV_PER_PAGE;
if (pages < 1) pages = 1;
if (gPage < 0) gPage = 0;
if (gPage >= pages) gPage = pages - 1;
integer start = gPage * AV_PER_PAGE;
integer end = start + AV_PER_PAGE - 1;
if (end > total - 1) end = total - 1;
list btn = [];
integer i;
for (i = start; i <= end; ++i) btn += [llList2String(gNearLabels, i)];
btn = padTo(btn, AV_PER_PAGE);
btn += navRow();
string msg =
"Select Follow Target\n"
+ "Range: " + (string)NEARBY_RANGE + " m\n"
+ "Found: " + (string)total + "\n"
+ "Page " + (string)(gPage + 1) + "/" + (string)pages;
llDialog(gOwner, msg, btn, gChan);
}
showFollowPick(integer page)
{
showFollowPickInternal(page, TRUE);
}
showFollowPickCached(integer page)
{
showFollowPickInternal(page, FALSE);
}
showLockPickInternal(integer page, integer rebuild)
{
pushBack(gState, gPage);
gState = M_LOCK_PICK;
gPage = page;
gCutMode = FALSE;
menuOpenListen();
if (rebuild) buildNearbyLists();
integer total = llGetListLength(gNearLabels);
integer pages = (total + AV_PER_PAGE - 1) / AV_PER_PAGE;
if (pages < 1) pages = 1;
if (gPage < 0) gPage = 0;
if (gPage >= pages) gPage = pages - 1;
integer start = gPage * AV_PER_PAGE;
integer end = start + AV_PER_PAGE - 1;
if (end > total - 1) end = total - 1;
list btn = [];
integer i;
for (i = start; i <= end; ++i) btn += [llList2String(gNearLabels, i)];
btn = padTo(btn, AV_PER_PAGE);
btn += navRow();
string msg =
"Lock Ziel wählen\n"
+ "Reichweite: " + (string)NEARBY_RANGE + " m\n"
+ "Gefunden: " + (string)total + "\n"
+ "Seite " + (string)(gPage + 1) + "/" + (string)pages;
llDialog(gOwner, msg, btn, gChan);
}
showLockPick(integer page)
{
showLockPickInternal(page, TRUE);
}
showLockPickCached(integer page)
{
showLockPickInternal(page, FALSE);
}
// ---- navigation dispatcher (previous menu) ----
showByState(integer st, integer pg)
{
integer beforeN = llGetListLength(gBackStack);
if (st == M_MAIN) { showMain(); return; }
else if (st == M_SAVE) { showSave(pg); }
else if (st == M_MOVETO) { showMoveTo(pg); }
else if (st == M_PLAY) { showPlay(pg); }
else if (st == M_SETTINGS) { showSettings(); }
else if (st == M_SETMODE) { showSetMode(); }
else if (st == M_SETDUR) { showSetDur(); }
else if (st == M_TOUR) { showTour(); }
else if (st == M_TOUR_PICK) { showTourPick(pg); }
else if (st == M_FOLLOW_PICK) { showFollowPickCached(pg); }
else if (st == M_LOCK_PICK) { showLockPickCached(pg); }
else { showMain(); return; }
integer afterN = llGetListLength(gBackStack);
if (afterN == beforeN + 2) {
gBackStack = llDeleteSubList(gBackStack, afterN - 2, afterN - 1);
}
}
// ---- LSL events ----
default
{
state_entry()
{
gOwner = llGetOwner();
}
on_rez(integer sp)
{
gOwner = llGetOwner();
}
attach(key id)
{
gOwner = llGetOwner();
if (id == NULL_KEY) menuClose();
}
touch_start(integer n)
{
key who = llDetectedKey(0);
if (!isOwner(who)) return;
showMain();
}
timer()
{
menuClose();
}
listen(integer channel, string name, key id, string msg)
{
if (channel != gChan) return;
if (!isOwner(id)) return;
// IMPORTANT: keep blanks " " non-functional:
// the trim turns " " into "", which we ignore.
msg = llStringTrim(msg, STRING_TRIM);
if (msg == "") return;
// Back goes to previous menu level
if (msg == "Back") {
if (!popBack()) { showMain(); return; }
showByState(gState, gPage);
return;
}
// Stable paging nav for paginated menus only
if (msg == "<<" || msg == ">>") {
integer pages = 1;
if (gState == M_SAVE) {
pages = (PRESET_MAX + SAVE_PER_PAGE - 1) / SAVE_PER_PAGE;
if (pages < 1) pages = 1;
if (msg == "<<") gPage = wrapPrev(gPage, pages);
else gPage = wrapNext(gPage, pages);
showByState(M_SAVE, gPage);
return;
}
if (gState == M_MOVETO) {
pages = (PRESET_MAX + MOVE_PER_PAGE - 1) / MOVE_PER_PAGE;
if (pages < 1) pages = 1;
if (msg == "<<") gPage = wrapPrev(gPage, pages);
else gPage = wrapNext(gPage, pages);
showByState(M_MOVETO, gPage);
return;
}
if (gState == M_PLAY) {
list cards = getShotNotecards();
integer total = llGetListLength(cards);
pages = (total + PLAY_PER_PAGE - 1) / PLAY_PER_PAGE;
if (pages < 1) pages = 1;
if (msg == "<<") gPage = wrapPrev(gPage, pages);
else gPage = wrapNext(gPage, pages);
showByState(M_PLAY, gPage);
return;
}
if (gState == M_TOUR_PICK) {
pages = (PRESET_MAX + TOUR_PER_PAGE - 1) / TOUR_PER_PAGE;
if (pages < 1) pages = 1;
if (msg == "<<") gPage = wrapPrev(gPage, pages);
else gPage = wrapNext(gPage, pages);
showByState(M_TOUR_PICK, gPage);
return;
}
if (gState == M_FOLLOW_PICK) {
integer totalF = llGetListLength(gNearLabels);
pages = (totalF + AV_PER_PAGE - 1) / AV_PER_PAGE;
if (pages < 1) pages = 1;
if (msg == "<<") gPage = wrapPrev(gPage, pages);
else gPage = wrapNext(gPage, pages);
showByState(M_FOLLOW_PICK, gPage);
return;
}
if (gState == M_LOCK_PICK) {
integer totalL = llGetListLength(gNearLabels);
pages = (totalL + AV_PER_PAGE - 1) / AV_PER_PAGE;
if (pages < 1) pages = 1;
if (msg == "<<") gPage = wrapPrev(gPage, pages);
else gPage = wrapNext(gPage, pages);
showByState(M_LOCK_PICK, gPage);
return;
}
// if not a paginated menu, ignore
return;
}
// ---- main ----
if (gState == M_MAIN) {
if (msg == "Save") { showSave(0); return; }
if (msg == "MoveTo") { showMoveTo(0); return; }
if (msg == "Play") { showPlay(0); return; }
if (msg == "Settings") { showSettings(); return; }
if (msg == "Tour") { showTour(); return; }
if (msg == "Show Cams") {
sendCamsShow();
gCamsShown = TRUE;
showMain(); // refresh label
return;
}
if (msg == "Hide Cams") {
sendCamsHide();
gCamsShown = FALSE;
showMain(); // refresh label
return;
}
if (msg == "Stop") { sendStop(); return; }
if (msg == "Follow") {
// lokalen Status nutzen (nicht sofort LSD neu lesen)
followLockLoadState();
if (gFollowOn) {
llMessageLinked(LINK_SET, MN_CMD, "FOLLOW|OFF", gOwner);
// sofort UI-korrekt
gFollowOn = FALSE; gFollowArg = "";
showMainEx(FALSE);
} else {
showFollowPick(0);
}
return;
}
if (msg == "Lock") {
followLockLoadState();
if (gLockOn) {
llMessageLinked(LINK_SET, MN_CMD, "LOCK|OFF", gOwner);
// sofort UI-korrekt
gLockOn = FALSE; gLockArg = "";
showMainEx(FALSE);
} else {
showLockPick(0);
}
return;
}
return;
}
// ---- save ----
if (gState == M_SAVE) {
integer idx = (integer)msg;
if (idx >= 1 && idx <= PRESET_MAX) {
sendSave(idx);
return;
}
return;
}
// ---- moveto ----
if (gState == M_MOVETO) {
if (msg == "Cut") { gCutMode = TRUE; showByState(M_MOVETO, gPage); return; }
if (msg == "Move") { gCutMode = FALSE; showByState(M_MOVETO, gPage); return; }
integer idx2 = (integer)msg;
if (idx2 >= 1 && idx2 <= PRESET_MAX) {
if (gCutMode) {
sendLoad(idx2);
gCutMode = FALSE;
} else {
sendMoveTo(idx2);
}
return;
}
return;
}
// ---- play ----
if (gState == M_PLAY) {
// msg is notecard name button
if (llGetInventoryType(msg) == INVENTORY_NOTECARD) {
sendPlay(msg);
return;
}
return;
}
// ---- settings ----
if (gState == M_SETTINGS) {
if (msg == "Mode") { showSetMode(); return; }
if (msg == "Duration") { showSetDur(); return; }
return;
}
// ---- set mode ----
if (gState == M_SETMODE) {
if (msg == "LINEAR") gMode = "linear";
else if (msg == "SPLINE") gMode = "spline";
else if (msg == "EASE_IN") gMode = "ease_in";
else if (msg == "EASE_OUT") gMode = "ease_out";
else if (msg == "EASE_IN_OUT") gMode = "ease_in_out";
// go back one level
if (!popBack()) { showMain(); return; }
showByState(gState, gPage);
return;
}
// ---- set duration ----
if (gState == M_SETDUR) {
if (msg == "Custom") {
llTextBox(gOwner, "Enter duration in ms (e.g. 3000):", gChan);
return;
}
integer d = (integer)msg;
if (d >= 200 && d <= 60000) gDurMs = d;
// return to previous level
if (!popBack()) { showMain(); return; }
showByState(gState, gPage);
return;
}
// ---- tour builder ----
if (gState == M_TOUR) {
if (msg == "Add") { showTourPick(0); return; }
if (msg == "Clear") { gTourIdx = []; showByState(M_TOUR, 0); return; }
if (msg == "Run") { sendTourRun(); return; }
return;
}
// ---- tour pick ----
if (gState == M_TOUR_PICK) {
integer idxT = (integer)msg;
if (idxT >= 1 && idxT <= PRESET_MAX) {
gTourIdx += [(string)idxT];
// Go back one level (to Tour builder) after adding
if (!popBack()) { showMain(); return; }
showByState(gState, gPage);
return;
}
return;
}
// ---- follow pick ----
if (gState == M_FOLLOW_PICK) {
integer idxF = llListFindList(gNearLabels, [msg]);
if (idxF < 0) return;
key kF = llList2Key(gNearKeys, idxF);
string nmF = llList2String(gNearNames, idxF);
say("Gewählt: " + nmF + " | UUID: " + (string)kF);
llMessageLinked(LINK_SET, MN_CMD, "FOLLOW|ON|" + (string)kF, gOwner);
// sofort UI korrekt + Mutual Exclusion im Menü
gFollowOn = TRUE; gFollowArg = (string)kF;
gLockOn = FALSE; gLockArg = "";
showMainEx(FALSE);
return;
}
// ---- lock pick ----
if (gState == M_LOCK_PICK) {
integer idxL = llListFindList(gNearLabels, [msg]);
if (idxL < 0) return;
key kL = llList2Key(gNearKeys, idxL);
string nmL = llList2String(gNearNames, idxL);
say("Gewählt: " + nmL + " | UUID: " + (string)kL);
llMessageLinked(LINK_SET, MN_CMD, "LOCK|ON|" + (string)kL, gOwner);
// sofort UI korrekt + Mutual Exclusion im Menü
gLockOn = TRUE; gLockArg = (string)kL;
gFollowOn = FALSE; gFollowArg = "";
showMainEx(FALSE);
return;
}
// If we got here, ignore.
}
}