1089 lines
29 KiB
Plaintext
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.
|
|
}
|
|
}
|