/* HS_DollyCam - TourEngine (offloaded) - Receives CE_CMD_TOUR from Controller - Requests current camera state from Core via CE_CMD_GET_STATE - Plays tour via timer and sends CE_INT_SET_CAM to Core */ integer CE_CMD_INIT = 1000; integer CE_CMD_TOUR = 1011; integer CE_CMD_STOP = 1012; integer CE_CMD_MOVE = 1010; integer CE_CMD_RELEASE = 1001; integer CE_CMD_GET_STATE = 1060; integer CE_CMD_CFG_DUMP = 1051; integer CE_CMD_FOV = 1040; // NEW: TourEngine -> Controller (payload: rad|quiet) 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; // Internal to Core integer CE_INT_SET_CAM = 3000; // | integer CE_INT_TOUR_BEGIN = 3001; // moveId integer CE_INT_TOUR_END = 3002; // moveId integer CE_INT_TOUR_STOP = 3003; integer DEBUG_FOV = FALSE; float gFovDbgNext = 0.0; float deg2rad(float d) { return d * PI / 180.0; } float rad2deg(float r) { return r * 180.0 / PI; } float gMOVE_STEP = 0.025; float gDEFAULT_FOCUS_DIST = 10.0; float gTourCamMinInterval = 0.033; // 30 Hz link-message cap for tour camera frames float gTourPosEps = 0.005; float gTourFocusEps = 0.005; key gOwner; integer gCamReady = FALSE; // ---- pending tour (waiting for state) ---- integer gPend = FALSE; string gPendReqId = ""; integer gPendMoveId = 0; integer gPendMoveMs = 0; string gPendMode = "linear"; integer gPendCount = 0; integer gPendStartFirst = FALSE; list gPendPos; list gPendFoc; list gPendHoldMs; list gPendWeight; integer gPendFovOn = FALSE; float gPendFovA = 0.0; // radians float gPendFovB = 0.0; // radians string gPipeParse = ""; integer gPipeAt = 0; integer gPipeLen = 0; // ---- TOUR runtime ---- integer gTourActive = FALSE; string gTourMode = "linear"; list gTourPos; // vectors (includes captured start) list gTourFoc; // vectors (includes captured start) list gTourHoldSec; // floats, hold AFTER each point (same length) list gTourSegLen; // floats, per segment (weighted) list gTourCumLen; // floats, per point cumulative length list gTourPointMoveT; // floats, per point time on "move clock" (excludes holds) integer gTourCount = 0; integer gTourLastSeg = 0; float gTourStart = 0.0; float gTourMoveDur = 0.0; float gTourHoldSum = 0.0; float gTourTotalDur = 0.0; float gTourPathLen = 0.0; // trapezoid motion params float gTourTe = 0.0; // accel duration float gTourTc = 0.0; // cruise duration float gTourVmax = 0.0; float gTourS1 = 0.0; float gTourS2 = 0.0; float gTourTeOut = 0.0; // decel duration (asymmetric) float TOUR_EASE_FRAC = 0.20; float gTourEaseInFrac = TOUR_EASE_FRAC; float gTourEaseOutFrac = TOUR_EASE_FRAC; integer gTourSpline = FALSE; integer gTourFovOn = FALSE; float gTourFovA = 0.0; // radians float gTourFovB = 0.0; // radians float gTourFovLastSend = 0.0; float gTourFovLastRad = -1.0; float gTourFovMinInterval = 0.10; // 10 Hz integer DEBUG_FOV_SEND = FALSE; // zum Verifizieren, danach auf FALSE string LSKEY_LOCK = "HS_LOCK"; vector KEEP_LOCK_OFFSET = <0,0,1.0>; integer gTourKeepOn = FALSE; vector gKeepFixedFocus = ZERO_VECTOR; // focA (oder first waypoint foc) vector gKeepMotifFocus = ZERO_VECTOR; // fixed oder lock-derived float gKeepLockNextPoll = 0.0; float gKeepLockPollInterval = 0.05; // 20 Hz lock tracking float gTourKeepK = 0.0; // K = d0 * tan(fov0/2) integer gHoldCamValid = FALSE; vector gHoldCamPos = ZERO_VECTOR; vector gHoldCamFoc = ZERO_VECTOR; float HOLD_CAM_EPS = 0.001; integer gCamSendValid = FALSE; float gCamSendLast = -999.0; float gCamSendNext = 0.0; vector gCamSendPos = ZERO_VECTOR; vector gCamSendFoc = ZERO_VECTOR; integer gSegCacheIdx = -1; float gSegCacheStart = 0.0; float gSegCacheEnd = 0.0; float gSegCacheLen = 0.0; vector gSegPos0 = ZERO_VECTOR; vector gSegPos1 = ZERO_VECTOR; vector gSegPos2 = ZERO_VECTOR; vector gSegPos3 = ZERO_VECTOR; vector gSegFoc0 = ZERO_VECTOR; vector gSegFoc1 = ZERO_VECTOR; vector gSegFoc2 = ZERO_VECTOR; vector gSegFoc3 = ZERO_VECTOR; // Debug (default OFF) integer DEBUG_KEEP = FALSE; dbgKeep(string s){ if (DEBUG_KEEP) llOwnerSay("[KEEP] " + s); } float clampFovRad(float rad) { // clamp via degrees 10..179 float deg = rad2deg(rad); deg = clampf(deg, 10.0, 179.0); return deg2rad(deg); } float keepFovRadFrom(vector pos, vector foc, float baseRad) { float d = llVecDist(pos, foc); if (d < 0.01) d = 0.01; if (gTourKeepK <= 0.000001) { return clampFovRad(baseRad); } // FOV = 2 * atan(K/d) -> use atan2(K, d) float rad = 2.0 * llAtan2(gTourKeepK, d); return clampFovRad(rad); } keepFovMaybeSend(vector pos, vector foc, float baseRad) { float rad = keepFovRadFrom(pos, foc, baseRad); float now = llGetTime(); if ((now - gTourFovLastSend) < gTourFovMinInterval) return; if (gTourFovLastRad >= 0.0 && llFabs(rad - gTourFovLastRad) < 0.002) return; gTourFovLastSend = now; gTourFovLastRad = rad; llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner); } keepFovSendNow(vector pos, vector foc, float baseRad) { float rad = keepFovRadFrom(pos, foc, baseRad); gTourFovLastSend = llGetTime(); gTourFovLastRad = rad; llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner); } // ---------- helpers ---------- float clampf(float v, float lo, float hi) { if (v < lo) return lo; if (v > hi) return hi; return v; } float clamp01(float x) { if (x < 0.0) return 0.0; if (x > 1.0) return 1.0; return x; } 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 pipeFieldCount(string s) { integer L = llStringLength(s); if (L < 1) return 0; integer count = 1; integer i; for (i = 0; i < L; ++i) { if (llGetSubString(s, i, i) == "|") ++count; } return count; } string pipeField(string s, integer want) { integer L = llStringLength(s); integer start = 0; integer idx = 0; while (start <= L) { integer end = findCharFrom(s, "|", start); if (end < 0) end = L; if (idx == want) return llGetSubString(s, start, end - 1); ++idx; start = end + 1; } return ""; } pipeParseBegin(string s) { gPipeParse = s; gPipeAt = 0; gPipeLen = llStringLength(s); } string pipeNext() { if (gPipeAt > gPipeLen) return ""; integer end = findCharFrom(gPipeParse, "|", gPipeAt); if (end < 0) end = gPipeLen; string v = llGetSubString(gPipeParse, gPipeAt, end - 1); gPipeAt = end + 1; return v; } pipeParseClear() { gPipeParse = ""; gPipeAt = 0; gPipeLen = 0; } tourFovMaybeSend(float frac) { if (!gTourFovOn) return; frac = clamp01(frac); float rad = gTourFovA + (gTourFovB - gTourFovA) * frac; float now = llGetTime(); if ((now - gTourFovLastSend) < gTourFovMinInterval) return; if (gTourFovLastRad >= 0.0 && llFabs(rad - gTourFovLastRad) < 0.002) return; gTourFovLastSend = now; gTourFovLastRad = rad; if (DEBUG_FOV_SEND) llOwnerSay("[FOV-DBG TOUR] payload-send frac=" + (string)frac + " rad=" + (string)rad); llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner); } tourFovForceFinal() { if (!gTourFovOn) return; float rad = gTourFovB; if (DEBUG_FOV_SEND) llOwnerSay("[FOV-DBG TOUR] payload-final rad=" + (string)rad); llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|1", gOwner); } integer tourSendCam(vector pos, vector foc, integer force) { float now = llGetTime(); if (!force && gCamSendValid) { if (gTourCamMinInterval > 0.0 && now < gCamSendNext) return FALSE; if (llVecDist(pos, gCamSendPos) <= gTourPosEps && llVecDist(foc, gCamSendFoc) <= gTourFocusEps) return FALSE; } gCamSendValid = TRUE; gCamSendLast = now; if (gTourCamMinInterval > 0.0) { if (force || gCamSendNext <= 0.0) gCamSendNext = now + gTourCamMinInterval; else { gCamSendNext += gTourCamMinInterval; if (gCamSendNext < now) gCamSendNext = now + gTourCamMinInterval; } } gCamSendPos = pos; gCamSendFoc = foc; llMessageLinked(LINK_SET, CE_INT_SET_CAM, (string)pos + "|" + (string)foc, gOwner); return TRUE; } tourSendHoldCam(vector pos, vector foc) { integer first = !gHoldCamValid; if (gHoldCamValid) { if (llVecDist(pos, gHoldCamPos) < HOLD_CAM_EPS && llVecDist(foc, gHoldCamFoc) < HOLD_CAM_EPS) return; } gHoldCamValid = TRUE; gHoldCamPos = pos; gHoldCamFoc = foc; tourSendCam(pos, foc, first); } tourCacheSegment(integer seg) { if (seg == gSegCacheIdx) return; if (gTourCount < 2) return; integer last = gTourCount - 1; integer i1 = seg; integer i2 = seg + 1; if (i1 < 0) i1 = 0; if (i2 > last) i2 = last; integer i0 = i1 - 1; if (i0 < 0) i0 = 0; integer i3 = i2 + 1; if (i3 > last) i3 = last; gSegCacheIdx = seg; gSegCacheStart = llList2Float(gTourCumLen, seg); gSegCacheEnd = llList2Float(gTourCumLen, seg + 1); gSegCacheLen = gSegCacheEnd - gSegCacheStart; gSegPos0 = llList2Vector(gTourPos, i0); gSegPos1 = llList2Vector(gTourPos, i1); gSegPos2 = llList2Vector(gTourPos, i2); gSegPos3 = llList2Vector(gTourPos, i3); gSegFoc0 = llList2Vector(gTourFoc, i0); gSegFoc1 = llList2Vector(gTourFoc, i1); gSegFoc2 = llList2Vector(gTourFoc, i2); gSegFoc3 = llList2Vector(gTourFoc, i3); } keepUpdateMotifFocus() { // default: fixed focus gKeepMotifFocus = gKeepFixedFocus; string s = llLinksetDataRead(LSKEY_LOCK); if (s == "") return; list p = llParseString2List(s, ["|"], []); integer L = llGetListLength(p); if (L < 1) return; integer on = (integer)llList2String(p, 0); if (!on) return; if (L < 2) return; string arg = llList2String(p, 1); // vector lock if (llGetSubString(arg, 0, 0) == "<") { gKeepMotifFocus = (vector)arg; return; } // key lock key k = (key)arg; if (k != NULL_KEY) { list d = llGetObjectDetails(k, [OBJECT_POS]); if (llGetListLength(d) >= 1) { vector tpos = llList2Vector(d, 0); gKeepMotifFocus = tpos + KEEP_LOCK_OFFSET; } return; } // fallback: treat as vector string gKeepMotifFocus = (vector)arg; } vector catmullRomVec(vector p0, vector p1, vector p2, vector p3, float t) { float t2 = t * t; float t3 = t2 * t; return 0.5 * ( (2.0 * p1) + (-p0 + p2) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3 ); } vector tourSplineAt(list pts, integer seg, float u) { integer last = llGetListLength(pts) - 1; integer i1 = seg; integer i2 = seg + 1; integer i0 = i1 - 1; if (i0 < 0) i0 = 0; integer i3 = i2 + 1; if (i3 > last) i3 = last; vector p0 = llList2Vector(pts, i0); vector p1 = llList2Vector(pts, i1); vector p2 = llList2Vector(pts, i2); vector p3 = llList2Vector(pts, i3); return catmullRomVec(p0, p1, p2, p3, u); } float tourDistAt(float t) { if (gTourMoveDur <= 0.0001 || gTourPathLen <= 0.0001 || gTourVmax <= 0.0001) return 0.0; t = clampf(t, 0.0, gTourMoveDur); if (gTourTe <= 0.0001 && gTourTeOut <= 0.0001) { return gTourPathLen * (t / gTourMoveDur); } float teIn = gTourTe; float teOut = gTourTeOut; if (t <= teIn) { return 0.5 * gTourVmax * (t * t / teIn); } if (t <= (teIn + gTourTc)) { return gTourS1 + gTourVmax * (t - teIn); } float t2 = t - (teIn + gTourTc); return gTourS2 + gTourVmax * t2 - 0.5 * gTourVmax * (t2 * t2 / teOut); } float tourTimeAtDist(float s) { if (gTourMoveDur <= 0.0001 || gTourPathLen <= 0.0001 || gTourVmax <= 0.0001) return 0.0; s = clampf(s, 0.0, gTourPathLen); if (gTourTe <= 0.0001 && gTourTeOut <= 0.0001) { return gTourMoveDur * (s / gTourPathLen); } float teIn = gTourTe; float teOut = gTourTeOut; if (s <= gTourS1) { return llSqrt((2.0 * s * teIn) / gTourVmax); } if (s <= gTourS2) { return teIn + ((s - gTourS1) / gTourVmax); } float s3 = s - gTourS2; float disc = (teOut * teOut) - ((2.0 * teOut * s3) / gTourVmax); if (disc < 0.0) disc = 0.0; float t2 = teOut - llSqrt(disc); return (teIn + gTourTc) + t2; } tourStopInternal() { gTourActive = FALSE; gPend = FALSE; gPendReqId = ""; gPendStartFirst = FALSE; gTourMode = "linear"; gTourPos = []; gTourFoc = []; gTourHoldSec = []; gTourSegLen = []; gTourCumLen = []; gTourPointMoveT = []; gTourCount = 0; gTourLastSeg = 0; gTourStart = 0.0; gTourMoveDur = 0.0; gTourHoldSum = 0.0; gTourTotalDur = 0.0; gTourPathLen = 0.0; gTourTe = gTourTc = gTourVmax = gTourS1 = gTourS2 = 0.0; gTourTeOut = 0.0; gTourSpline = FALSE; gTourEaseInFrac = TOUR_EASE_FRAC; gTourEaseOutFrac = TOUR_EASE_FRAC; gPendFovOn = FALSE; gPendFovA = 0.0; gPendFovB = 0.0; gTourFovOn = FALSE; gTourFovA = 0.0; gTourFovB = 0.0; gTourFovLastSend = 0.0; gTourFovLastRad = -1.0; gTourKeepOn = FALSE; gTourKeepK = 0.0; gKeepFixedFocus = ZERO_VECTOR; gKeepMotifFocus = ZERO_VECTOR; gKeepLockNextPoll = 0.0; gHoldCamValid = FALSE; gHoldCamPos = ZERO_VECTOR; gHoldCamFoc = ZERO_VECTOR; gCamSendValid = FALSE; gCamSendLast = -999.0; gCamSendNext = 0.0; gCamSendPos = ZERO_VECTOR; gCamSendFoc = ZERO_VECTOR; gSegCacheIdx = -1; gSegCacheStart = 0.0; gSegCacheEnd = 0.0; gSegCacheLen = 0.0; gSegPos0 = ZERO_VECTOR; gSegPos1 = ZERO_VECTOR; gSegPos2 = ZERO_VECTOR; gSegPos3 = ZERO_VECTOR; gSegFoc0 = ZERO_VECTOR; gSegFoc1 = ZERO_VECTOR; gSegFoc2 = ZERO_VECTOR; gSegFoc3 = ZERO_VECTOR; llSetTimerEvent(0.0); } applyCfgDump(string dump) { // dump format: key=val|key=val|... list parts = llParseString2List(dump, ["|"], []); integer i; for (i = 0; i < llGetListLength(parts); ++i) { string kv = llList2String(parts, i); integer eq = llSubStringIndex(kv, "="); if (eq < 1) jump next; string k = llToLower(llStringTrim(llGetSubString(kv, 0, eq - 1), STRING_TRIM)); string v = llStringTrim(llGetSubString(kv, eq + 1, -1), STRING_TRIM); if (k == "move_step") gMOVE_STEP = (float)v; else if (k == "default_focus_dist") gDEFAULT_FOCUS_DIST = (float)v; else if (k == "tour_cam_min_interval") gTourCamMinInterval = clampf((float)v, 0.0, 0.25); else if (k == "tour_pos_epsilon") gTourPosEps = clampf((float)v, 0.0, 1.0); else if (k == "tour_focus_epsilon") gTourFocusEps = clampf((float)v, 0.0, 1.0); @next; } if (gTourActive) llSetTimerEvent(gMOVE_STEP); } startTourFromState(vector startPos, vector startFoc) { // ---- FIX: preserve pending FOV before runtime reset ---- integer keepFovOn = gPendFovOn; float keepFovA = gPendFovA; float keepFovB = gPendFovB; integer keepStartFirst = gPendStartFirst; // Cancel any previous tour runtime tourStopInternal(); // restore into runtime (tourStopInternal may clear pend vars) gTourFovOn = keepFovOn; gTourFovA = keepFovA; gTourFovB = keepFovB; gPendStartFirst = keepStartFirst; // Mode parsing (supports "spline+ease_in+keep") string m = llToLower(gPendMode); integer useSpline = FALSE; string prof = "linear"; gTourEaseInFrac = TOUR_EASE_FRAC; gTourEaseOutFrac = TOUR_EASE_FRAC; integer wantKeep = FALSE; list partsM = llParseString2List(m, ["+"], []); integer mi; for (mi = 0; mi < llGetListLength(partsM); ++mi) { string tok = llStringTrim(llList2String(partsM, mi), STRING_TRIM); if (tok == "") jump mnext; if (tok == "keep" || tok == "keepframe" || tok == "kf") { wantKeep = TRUE; jump mnext; } if (tok == "spline") useSpline = TRUE; else if (tok == "linear" || tok == "line") prof = "linear"; else if (tok == "constant" || tok == "noease" || tok == "no_ease") { prof = "constant"; gTourEaseInFrac = 0.0; gTourEaseOutFrac = 0.0; } else if (tok == "ease_in" || tok == "easein") { prof = "ease_in"; gTourEaseInFrac = 0.35; gTourEaseOutFrac = 0.15; } else if (tok == "ease_out" || tok == "easeout") { prof = "ease_out"; gTourEaseInFrac = 0.15; gTourEaseOutFrac = 0.35; } else if (tok == "ease_in_out" || tok == "easeinout" || tok == "ease-in-out") { prof = "ease_in_out"; gTourEaseInFrac = 0.30; gTourEaseOutFrac = 0.30; } else if (tok == "smoothstep") { prof = "smoothstep"; gTourEaseInFrac = 0.45; gTourEaseOutFrac = 0.45; } else if (tok == "cubic") { prof = "cubic"; gTourEaseInFrac = 0.25; gTourEaseOutFrac = 0.25; } else if (tok == "quint" || tok == "quintic") { prof = "quint"; gTourEaseInFrac = 0.40; gTourEaseOutFrac = 0.40; } @mnext; } gTourKeepOn = wantKeep; gTourSpline = useSpline; if (gTourSpline) { if (prof == "linear") gTourMode = "spline"; else gTourMode = "spline+" + prof; } else { if (prof == "linear") gTourMode = "linear"; else gTourMode = prof; } if (gPendStartFirst) { gTourPos = gPendPos; gTourFoc = gPendFoc; } else { gTourPos = [startPos] + gPendPos; gTourFoc = [startFoc] + gPendFoc; } // ===== KEEP: choose fixed focus (prefer focA = first waypoint focus) and pick motif focus (lock if on) ===== if (gTourKeepOn) { gKeepFixedFocus = startFoc; if (llGetListLength(gPendFoc) > 0) { gKeepFixedFocus = llList2Vector(gPendFoc, 0); // focA } keepUpdateMotifFocus(); gKeepLockNextPoll = llGetTime() + gKeepLockPollInterval; // compute K from startPos and CURRENT motif focus float baseRad0 = gTourFovA; if (!gTourFovOn || baseRad0 <= 0.0001) baseRad0 = deg2rad(60.0); float d0 = llVecDist(startPos, gKeepMotifFocus); if (d0 < 0.01) d0 = 0.01; gTourKeepK = d0 * llTan(baseRad0 * 0.5); dbgKeep("keep ON baseDeg=" + (string)rad2deg(baseRad0) + " d0=" + (string)d0 + " K=" + (string)gTourKeepK); } if (gPendStartFirst) gTourHoldSec = []; else gTourHoldSec = [0.0]; integer i; for (i = 0; i < llGetListLength(gPendHoldMs); ++i) { float hs = (float)llList2Integer(gPendHoldMs, i) / 1000.0; if (hs < 0.0) hs = 0.0; gTourHoldSec += [hs]; } gTourCount = llGetListLength(gTourPos); gTourMoveDur = (float)gPendMoveMs / 1000.0; if (gTourMoveDur < 0.0) gTourMoveDur = 0.0; // Sum holds gTourHoldSum = 0.0; for (i = 0; i < gTourCount; ++i) gTourHoldSum += llList2Float(gTourHoldSec, i); gTourTotalDur = gTourMoveDur + gTourHoldSum; if (gTourTotalDur < 0.01) gTourTotalDur = 0.01; // Segment lengths (weighted) gTourSegLen = []; gTourCumLen = [0.0]; gTourPathLen = 0.0; for (i = 0; i < gTourCount - 1; ++i) { float L = llVecDist(llList2Vector(gTourPos, i), llList2Vector(gTourPos, i + 1)); if (L < 0.000001) L = 0.0; float w = 1.0; if (i < llGetListLength(gPendWeight)) w = llList2Float(gPendWeight, i); w = clampf(w, 0.10, 10.0); float WL = L * w; gTourSegLen += [WL]; gTourPathLen += WL; gTourCumLen += [gTourPathLen]; } // Trapezoid profile params (movement only), asymmetric ease if (gTourMoveDur > 0.0001 && gTourPathLen > 0.0001) { float teIn = gTourMoveDur * gTourEaseInFrac; float teOut = gTourMoveDur * gTourEaseOutFrac; if (teIn > 0.0 && teIn < 0.10) teIn = 0.10; if (teOut > 0.0 && teOut < 0.10) teOut = 0.10; float maxTe = gTourMoveDur * 0.45; if (teIn > maxTe) teIn = maxTe; if (teOut > maxTe) teOut = maxTe; if (teIn < 0.01) teIn = 0.0; if (teOut < 0.01) teOut = 0.0; if ((teIn + teOut) > gTourMoveDur && (teIn + teOut) > 0.0001) { float sc = gTourMoveDur / (teIn + teOut); teIn *= sc; teOut *= sc; gTourTc = 0.0; } else { gTourTc = gTourMoveDur - teIn - teOut; if (gTourTc < 0.0) gTourTc = 0.0; } gTourTe = teIn; gTourTeOut = teOut; float denom = gTourTc + 0.5 * (gTourTe + gTourTeOut); if (denom < 0.01) denom = 0.01; gTourVmax = gTourPathLen / denom; gTourS1 = 0.5 * gTourVmax * gTourTe; gTourS2 = gTourS1 + (gTourVmax * gTourTc); } else { gTourTe = gTourTc = gTourVmax = gTourS1 = gTourS2 = 0.0; gTourTeOut = 0.0; } // Arrival times on move clock gTourPointMoveT = []; if (gTourMoveDur <= 0.0001 || gTourPathLen <= 0.0001) { for (i = 0; i < gTourCount; ++i) gTourPointMoveT += [0.0]; } else { for (i = 0; i < gTourCount; ++i) { float dist = llList2Float(gTourCumLen, i); gTourPointMoveT += [tourTimeAtDist(dist)]; } gTourPointMoveT = llListReplaceList(gTourPointMoveT, [gTourMoveDur], gTourCount - 1, gTourCount - 1); } // Start runtime gTourStart = llGetTime(); gTourLastSeg = 0; gTourActive = TRUE; // Tell Core: begin external drive + set first frame llMessageLinked(LINK_SET, CE_INT_TOUR_BEGIN, (string)gPendMoveId, gOwner); vector startFocSend = startFoc; if (gTourKeepOn) startFocSend = gKeepMotifFocus; tourSendCam(startPos, startFocSend, TRUE); // send initial FOV (quiet) if (gTourKeepOn) { float baseRad1 = gTourFovA; if (!gTourFovOn || baseRad1 <= 0.0001) baseRad1 = deg2rad(60.0); keepFovSendNow(startPos, startFocSend, baseRad1); } else if (gTourFovOn) { llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTourFovA + "|1", gOwner); gTourFovLastSend = llGetTime(); gTourFovLastRad = gTourFovA; } llSetTimerEvent(gMOVE_STEP); gPend = FALSE; gPendReqId = ""; gPendPos = []; gPendFoc = []; gPendHoldMs = []; gPendWeight = []; gPendStartFirst = FALSE; } requestStateForPending() { gPendReqId = "TOUR:" + (string)gPendMoveId + ":" + (string)llGetUnixTime(); llMessageLinked(LINK_SET, CE_CMD_GET_STATE, gPendReqId, gOwner); } startPendingAtFirst() { if (!gPend || llGetListLength(gPendPos) < 1 || llGetListLength(gPendFoc) < 1) return; startTourFromState(llList2Vector(gPendPos, 0), llList2Vector(gPendFoc, 0)); } default { state_entry() { gOwner = llGetOwner(); // cfg sync (move_step etc.) llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); } on_rez(integer sp) { gOwner = llGetOwner(); tourStopInternal(); } attach(key id) { gOwner = llGetOwner(); if (id == NULL_KEY) { if (gTourActive || gPend) tourStopInternal(); } } timer() { if (!gTourActive) { llSetTimerEvent(0.0); return; } // ---- DEBUG: periodic FOV ticks during ANY active tour (ignore payload for now) ---- if (DEBUG_FOV) { float nowD = llGetTime(); if (nowD >= gFovDbgNext) { gFovDbgNext = nowD + 0.25; // 4 Hz float fracD = 0.0; if (gTourTotalDur > 0.001) fracD = (nowD - gTourStart) / gTourTotalDur; if (fracD < 0.0) fracD = 0.0; if (fracD > 1.0) fracD = 1.0; float radD = deg2rad(30.0 + (90.0 - 30.0) * fracD); llOwnerSay("[FOV-DBG TOUR] send tick frac=" + (string)fracD + " rad=" + (string)radD + " deg=" + (string)rad2deg(radD)); llMessageLinked(LINK_SET, CE_CMD_FOV, (string)radD + "|1", gOwner); } } float now = llGetTime(); float elapsed = now - gTourStart; // baseRad used for keepframe (start FOV) float baseRad = gTourFovA; if (!gTourFovOn || baseRad <= 0.0001) baseRad = deg2rad(60.0); if (elapsed >= gTourTotalDur) { vector endPos = llList2Vector(gTourPos, gTourCount - 1); vector endFoc = llList2Vector(gTourFoc, gTourCount - 1); if (gTourKeepOn) { keepUpdateMotifFocus(); endFoc = gKeepMotifFocus; } tourSendCam(endPos, endFoc, TRUE); llMessageLinked(LINK_SET, CE_INT_TOUR_END, (string)gPendMoveId, gOwner); llMessageLinked(LINK_SET, CE_EVT_MOVE_DONE, (string)gPendMoveId, gOwner); if (gTourKeepOn) { keepFovSendNow(endPos, endFoc, baseRad); } else { tourFovForceFinal(); } tourStopInternal(); return; } // Determine holds (elapsed is overall timeline) float holdPassed = 0.0; integer holdIdx = -1; integer i; for (i = 0; i < gTourCount; ++i) { float tArr = llList2Float(gTourPointMoveT, i) + holdPassed; float h = llList2Float(gTourHoldSec, i); if (h > 0.0) { if (elapsed >= tArr && elapsed < (tArr + h)) { holdIdx = i; jump foundHold; } if (elapsed >= (tArr + h)) holdPassed += h; } } @foundHold; if (holdIdx != -1) { vector hp = llList2Vector(gTourPos, holdIdx); vector hf = llList2Vector(gTourFoc, holdIdx); if (gTourKeepOn) { if (now >= gKeepLockNextPoll) { keepUpdateMotifFocus(); gKeepLockNextPoll = now + gKeepLockPollInterval; } hf = gKeepMotifFocus; } tourSendHoldCam(hp, hf); if (gTourKeepOn) { keepFovMaybeSend(hp, hf, baseRad); } return; } float moveT = elapsed - holdPassed; gHoldCamValid = FALSE; if (moveT < 0.0) moveT = 0.0; if (moveT > gTourMoveDur) moveT = gTourMoveDur; float dist; if (gTourMoveDur <= 0.0001) dist = gTourPathLen; else dist = tourDistAt(moveT); integer segCount = gTourCount - 1; integer seg = gTourLastSeg; if (seg < 0) seg = 0; if (seg >= segCount) seg = segCount - 1; while (seg < segCount - 1 && dist > llList2Float(gTourCumLen, seg + 1) + 0.0001) ++seg; while (seg > 0 && dist < llList2Float(gTourCumLen, seg) - 0.0001) --seg; gTourLastSeg = seg; tourCacheSegment(seg); float u = 0.0; if (gSegCacheLen > 0.000001) u = (dist - gSegCacheStart) / gSegCacheLen; u = clampf(u, 0.0, 1.0); vector pos; vector foc; if (gTourSpline) { pos = catmullRomVec(gSegPos0, gSegPos1, gSegPos2, gSegPos3, u); foc = catmullRomVec(gSegFoc0, gSegFoc1, gSegFoc2, gSegFoc3, u); } else { pos = gSegPos1 + (gSegPos2 - gSegPos1) * u; foc = gSegFoc1 + (gSegFoc2 - gSegFoc1) * u; } if (gTourKeepOn) { if (now >= gKeepLockNextPoll) { keepUpdateMotifFocus(); gKeepLockNextPoll = now + gKeepLockPollInterval; } foc = gKeepMotifFocus; } tourSendCam(pos, foc, FALSE); // FOV: keepframe or ramp float frac = 1.0; if (gTourPathLen > 0.0001) frac = dist / gTourPathLen; else if (gTourMoveDur > 0.0001) frac = moveT / gTourMoveDur; if (gTourKeepOn) { keepFovMaybeSend(pos, foc, baseRad); } else { tourFovMaybeSend(frac); } } link_message(integer sender, integer num, string str, key id) { if (num == CE_INT_TOUR_STOP) { tourStopInternal(); return; } if (num == CE_EVT_READY) { gCamReady = TRUE; if (gPend) { if (gPendStartFirst) startPendingAtFirst(); else requestStateForPending(); } return; } if (num == CE_EVT_DENIED) { gCamReady = FALSE; return; } if (num == CE_EVT_CFG_DUMP) { applyCfgDump(str); return; } if (num == CE_CMD_RELEASE) { // Core sendet CE_INT_TOUR_STOP ohnehin (und/oder wir stoppen uns selbst) tourStopInternal(); return; } if (num == CE_CMD_STOP) { tourStopInternal(); return; } // Move überschreibt Tour: nur uns selbst stoppen – NICHT den Core stoppen if (num == CE_CMD_MOVE) { if (gTourActive || gPend) tourStopInternal(); return; } if (num == CE_EVT_STATE) { // payload: reqId||| if (pipeFieldCount(str) < 4) return; string reqId = pipeField(str, 0); if (!gPend || reqId != gPendReqId) return; vector startPos = (vector)pipeField(str, 1); vector startFoc = (vector)pipeField(str, 2); // If Core returned zeros (very early), fall back to first waypoint if (startPos == ZERO_VECTOR && llGetListLength(gPendPos) > 0) { startPos = llList2Vector(gPendPos, 0); startFoc = llList2Vector(gPendFoc, 0); } startTourFromState(startPos, startFoc); return; } if (num == CE_CMD_TOUR) { integer len = pipeFieldCount(str); if (len < 4) return; pipeParseBegin(str); integer mid = (integer)pipeNext(); integer dms = (integer)pipeNext(); string mode = pipeNext(); integer count = (integer)pipeNext(); if (count < 2) { pipeParseClear(); llMessageLinked(LINK_SET, CE_EVT_MOVE_DONE, (string)mid, gOwner); return; } // Optional header extension: |FOV||| integer base = 4; integer fovOn = FALSE; float fovA = 0.0; float fovB = 0.0; string next = pipeNext(); if (len >= 7 && llToUpper(next) == "FOV") { fovOn = TRUE; fovA = (float)pipeNext(); fovB = (float)pipeNext(); base = 7; } else { gPipeAt = 0; pipeNext(); pipeNext(); pipeNext(); pipeNext(); } integer per = 3; integer needed3 = base + (count * 3); integer needed4 = base + (count * 4); if (len >= needed4) per = 4; else if (len < needed3) { pipeParseClear(); return; } list posIn = []; list focIn = []; list holdIn = []; list wIn = []; integer i; for (i = 0; i < count; ++i) { vector pos = (vector)pipeNext(); vector foc = (vector)pipeNext(); integer hms = (integer)pipeNext(); if (hms < 0) hms = 0; posIn += [pos]; focIn += [foc]; holdIn += [hms]; if (per == 4) { float w = (float)pipeNext(); wIn += [w]; } } pipeParseClear(); // store pending + stop our current playback tourStopInternal(); gPend = TRUE; gPendMoveId = mid; gPendMoveMs = dms; gPendMode = mode; gPendCount = count; gPendStartFirst = (llSubStringIndex(llToLower(mode), "startfirst") >= 0); gPendPos = posIn; gPendFoc = focIn; 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; } } }