/* 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 "") // 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 "") 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. } }