HSDollyCam/HS_CamEngineTour.lsl

1167 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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; // <pos>|<focus>
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.002;
float gTourFocusEps = 0.002;
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|<pos>|<focus>|<rot>
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|<radA>|<radB>|
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;
}
}
}