/* HS_DollyCam - Tour command helper - Handles secondary chat/menu one-shot tour builders - Keeps HS_CamPlaylist.lsl focused on memory-sensitive notecard playback */ // Engine protocol integer CE_CMD_TOUR = 1011; integer CE_CMD_CFG_DUMP = 1051; integer CE_EVT_CFG_DUMP = 2051; // Controller -> helper protocol integer PH_CMD_CHAT_TOUR = 6102; // payload: full chat line starting with "tour ..." integer PH_CMD_TOURRUN = 6103; // payload: raw menu string "TOURRUN|..." integer PH_CMD_CHAT_DZ = 6104; // payload: full chat line starting with "dollyzoom ..." // Presets string PRE_KEY(integer idx) { return "P" + (string)idx; } key gOwner; integer gMoveId = 8000; integer gDefaultMoveMs = 2200; integer gTourMaxPoints = 20; vector gTmpPos; vector gTmpFoc; integer gTmpHasFov = FALSE; float gTmpFovRad = 0.0; integer nextMoveId() { ++gMoveId; return gMoveId; } integer isValidIdx(integer idx) { return (idx > 0); } say(string s) { llOwnerSay(s); } hudHide() { llSetAlpha(0.0, ALL_SIDES); } float deg2rad(float deg) { return deg * PI / 180.0; } 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 = rad * 180.0 / PI; deg = clampf(deg, 10.0, 179.0); return deg2rad(deg); } string modeSanitize(string raw) { raw = llToLower(llStringTrim(raw, STRING_TRIM)); if (raw == "") return "linear"; return raw; } integer findCharFrom(string s, string ch, integer start) { integer L = llStringLength(s); integer i; for (i = start; i < L; ++i) { if (llGetSubString(s, i, i) == ch) return i; } return -1; } integer cfgGetInt(string dump, string keyName, integer def) { integer at = llSubStringIndex(dump, keyName); if (at < 0) return def; at += llStringLength(keyName); integer end = findCharFrom(dump, "|", at); string v = ""; if (end < 0) v = llGetSubString(dump, at, -1); else v = llGetSubString(dump, at, end - 1); v = llStringTrim(v, STRING_TRIM); if (v == "") return def; return (integer)v; } 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|fovRad integer start = 0; integer end = 0; integer field = 0; float px = 0.0; float py = 0.0; float pz = 0.0; float fx = 0.0; float fy = 0.0; float fz = 0.0; float fr = 0.0; while (field <= 10) { end = findCharFrom(data, "|", start); string v = ""; if (end < 0) v = llGetSubString(data, start, -1); else v = llGetSubString(data, start, end - 1); if (field <= 5 && v == "") return FALSE; if (field == 0) px = (float)v; else if (field == 1) py = (float)v; else if (field == 2) pz = (float)v; else if (field == 3) fx = (float)v; else if (field == 4) fy = (float)v; else if (field == 5) fz = (float)v; else if (field == 10) fr = (float)v; ++field; if (end < 0) jump preset_done; start = end + 1; } @preset_done; if (field < 6) return FALSE; gTmpPos = ; gTmpFoc = ; gTmpHasFov = FALSE; gTmpFovRad = 0.0; if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); } return TRUE; } list tourParseFov(list t, integer startIdx) { integer n = llGetListLength(t); integer i; for (i = startIdx; i < n; ++i) { string tok = llToLower(llList2String(t, i)); if ((tok == "fovdeg" || tok == "fov") && (i + 2 < n)) { float a = (float)llList2String(t, i + 1); float b = (float)llList2String(t, i + 2); if (tok == "fovdeg") { a = clampf(a, 10.0, 179.0); b = clampf(b, 10.0, 179.0); return [1, deg2rad(a), deg2rad(b)]; } float adeg = clampf(a * 180.0 / PI, 10.0, 179.0); float bdeg = clampf(b * 180.0 / PI, 10.0, 179.0); return [1, deg2rad(adeg), deg2rad(bdeg)]; } } return [0, 0.0, 0.0]; } runChatTour(string msg) { msg = llStringTrim(msg, STRING_TRIM); if (llToLower(llGetSubString(msg, 0, 3)) != "tour") return; if (llStringLength(msg) > 600) { say("Tour one-liner too long. Use notecard tour blocks."); return; } list t = llParseString2List(msg, [" "], []); integer n = llGetListLength(t); if (n < 4) { say("Tour: usage: tour [mode] ..."); return; } integer totalMs = (integer)llList2String(t, 1); if (totalMs < 0) totalMs = 0; integer i = 2; string mode = "linear"; string tok2 = llList2String(t, 2); integer maybeIdx = (integer)tok2; if (maybeIdx <= 0 || (string)maybeIdx != tok2) { mode = tok2; i = 3; } mode = modeSanitize(mode); list posList = []; list focList = []; integer k = i; integer fovAt = -1; for (; k < n; ++k) { string tok = llToLower(llList2String(t, k)); if (tok == "fovdeg" || tok == "fov") { fovAt = k; jump chat_idx_done; } integer idx = (integer)llList2String(t, k); if (!isValidIdx(idx) || !loadPreset(idx)) { say("Tour: bad/missing preset: " + llList2String(t, k)); return; } posList += [gTmpPos]; focList += [gTmpFoc]; } @chat_idx_done; integer want = llGetListLength(posList); if (want < 2) { say("Tour: needs at least 2 presets."); return; } if (want > gTourMaxPoints) { say("Tour: too many points, capping to " + (string)gTourMaxPoints); want = gTourMaxPoints; posList = llList2List(posList, 0, want - 1); focList = llList2List(focList, 0, want - 1); } integer fovOn = FALSE; float fovA = 0.0; float fovB = 0.0; if (fovAt != -1) { list ff2 = tourParseFov(t, fovAt); fovOn = (integer)llList2String(ff2, 0); fovA = llList2Float(ff2, 1); fovB = llList2Float(ff2, 2); } integer mid = nextMoveId(); string payload = (string)mid + "|" + (string)totalMs + "|" + mode + "|" + (string)want; if (fovOn) payload += "|FOV|" + (string)fovA + "|" + (string)fovB; integer j; for (j = 0; j < want; ++j) { payload += "|" + (string)llList2Vector(posList, j) + "|" + (string)llList2Vector(focList, j) + "|0"; } hudHide(); llMessageLinked(LINK_SET, CE_CMD_TOUR, payload, gOwner); } runChatDollyZoom(string msg) { msg = llStringTrim(msg, STRING_TRIM); if (llToLower(llGetSubString(msg, 0, 8)) != "dollyzoom") return; if (llStringLength(msg) > 600) { say("DollyZoom one-liner too long. Use notecard/playlist."); return; } list t = llParseString2List(msg, [" "], []); integer n = llGetListLength(t); if (n < 4) { say("DollyZoom: usage: dollyzoom [mode] "); return; } integer totalMs = (integer)llList2String(t, 1); if (totalMs < 0) totalMs = 0; integer i = 2; string mode = "linear"; string tok2 = llList2String(t, 2); integer maybeIdx = (integer)tok2; if (maybeIdx <= 0 || (string)maybeIdx != tok2) { mode = tok2; i = 3; } mode = modeSanitize(mode); if (i + 1 >= n) { say("DollyZoom: usage: dollyzoom [mode] "); return; } integer idxA = (integer)llList2String(t, i); integer idxB = (integer)llList2String(t, i + 1); integer keepOn = FALSE; integer k; for (k = i + 2; k < n; ++k) { string tokK = llToLower(llList2String(t, k)); if (tokK == "keep" || tokK == "keepframe" || tokK == "kf") { keepOn = TRUE; } } if (keepOn) mode = mode + "+keep"; if (!isValidIdx(idxA) || !isValidIdx(idxB)) { say("DollyZoom: idx must be > 0"); return; } if (!loadPreset(idxA)) { say("DollyZoom: missing preset " + (string)idxA); return; } vector posA = gTmpPos; vector focA = gTmpFoc; integer hasA = gTmpHasFov; float fovA = gTmpFovRad; if (!loadPreset(idxB)) { say("DollyZoom: missing preset " + (string)idxB); return; } vector posB = gTmpPos; vector focB = gTmpFoc; integer hasB = gTmpHasFov; float fovB = gTmpFovRad; if (keepOn) focB = focA; if (!hasA || (!keepOn && !hasB)) { say("DollyZoom needs FOV stored in BOTH presets (unless using 'keep'). Set FOV (fovdeg) and save presets again."); return; } if (keepOn && !hasB) fovB = fovA; integer mid = nextMoveId(); string payload = (string)mid + "|" + (string)totalMs + "|" + mode + "|2" + "|FOV|" + (string)fovA + "|" + (string)fovB + "|" + (string)posA + "|" + (string)focA + "|0" + "|" + (string)posB + "|" + (string)focB + "|0"; hudHide(); llMessageLinked(LINK_SET, CE_CMD_TOUR, payload, gOwner); } runMenuTourRun(string raw) { // raw: "TOURRUN|totalMs|mode|count|idx1|idx2|..." if (llStringLength(raw) > 1800) { say("Tour: menu payload too large."); return; } list p = llParseString2List(raw, ["|"], []); integer len = llGetListLength(p); if (len < 5) return; if (llToUpper(llList2String(p, 0)) != "TOURRUN") return; integer totalMs = (integer)llList2String(p, 1); string mode = modeSanitize(llList2String(p, 2)); integer count = (integer)llList2String(p, 3); if (totalMs < 1) totalMs = gDefaultMoveMs; if (count < 2) { say("Tour: needs at least 2 points."); return; } if (count > gTourMaxPoints) { say("Tour: too many points, capping to " + (string)gTourMaxPoints); count = gTourMaxPoints; } if (len < (4 + count)) { say("Tour: bad menu payload."); return; } integer mid = nextMoveId(); string payload = (string)mid + "|" + (string)totalMs + "|" + mode + "|" + (string)count; integer i; for (i = 0; i < count; ++i) { integer idx = (integer)llList2String(p, 4 + i); if (!isValidIdx(idx) || !loadPreset(idx)) { say("Tour: bad/missing preset: " + (string)idx); return; } payload += "|" + (string)gTmpPos + "|" + (string)gTmpFoc + "|0"; } hudHide(); llMessageLinked(LINK_SET, CE_CMD_TOUR, payload, gOwner); } default { state_entry() { gOwner = llGetOwner(); llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); } on_rez(integer sp) { gOwner = llGetOwner(); } attach(key id) { gOwner = llGetOwner(); } link_message(integer sender, integer num, string str, key id) { if (num == CE_EVT_CFG_DUMP) { gDefaultMoveMs = cfgGetInt(str, "default_move_ms=", gDefaultMoveMs); integer n = cfgGetInt(str, "tour_max_points=", gTourMaxPoints); if (n < 2) n = 2; if (n > 40) n = 40; gTourMaxPoints = n; return; } if (id != gOwner) return; if (num == PH_CMD_CHAT_TOUR) { runChatTour(str); return; } if (num == PH_CMD_CHAT_DZ) { runChatDollyZoom(str); return; } if (num == PH_CMD_TOURRUN) { runMenuTourRun(str); return; } } }