/* HS_DollyCam - FOV Helper - Central place that actually sends RLVa @setcam_fov - Receives CE_CMD_FOV link messages: "rad|quiet" or "rad|quiet|flags" - Dedupe + optional force flag (and optional sync hook via CE_CMD_GET_STATE if you want later) Put this script into the HUD linkset. */ integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional) integer CE_CMD_RELEASE = 1001; // for cleanup integer CE_CMD_GET_STATE = 1060; // optional sync (not required for basic) integer CE_EVT_STATE = 2060; // optional sync response // --- RLVa --- string RLV_FOV_CMD = "setcam_fov"; // if your viewer uses another name, change here float RLV_FOV_MIN_DEG = 10.0; float RLV_FOV_MAX_DEG = 179.0; // allow wide values (e.g. rad ~ 3.0) // flags (optional third field) integer FOVF_SYNC = 1; // request state before deciding (optional) integer FOVF_FORCE = 2; // bypass dedupe // runtime key gOwner; float gLastSentRad = -1.0; float gEpsRad = 0.0005; // ~0.03deg float gMinInterval = 0.02; // small safety throttle (senders already throttle) float gLastSendT = 0.0; // optional sync integer gSyncPend = FALSE; string gSyncReq = ""; float gSyncRad = 0.0; integer gSyncQuiet= 1; integer gSyncFlags= 0; // debug (keep OFF in production) integer DEBUG_FOV = FALSE; dbg(string s){ if (DEBUG_FOV) llOwnerSay("[FOV] " + s); } float clampf(float v, float lo, float hi){ if (vhi) return hi; return v; } float deg2rad(float d){ return d * PI / 180.0; } float rad2deg(float r){ return r * 180.0 / PI; } float clampFovRad(float rad) { float deg = rad2deg(rad); deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG); return deg2rad(deg); } integer camTrackOk() { return ((llGetPermissions() & PERMISSION_TRACK_CAMERA) && (llGetPermissionsKey() == gOwner)); } float getViewerFovRadOrNeg() { if (!camTrackOk()) return -1.0; float f = llGetCameraFOV(); if (f <= 0.0001) return -1.0; return clampFovRad(f); } sendRlvaFov(float rad, integer quiet, integer flags) { rad = clampFovRad(rad); float now = llGetTime(); if ((now - gLastSendT) < gMinInterval) { // tiny global throttle (prevents double-sends from multiple sources same frame) if (!(flags & FOVF_FORCE)) return; } if (!(flags & FOVF_FORCE)) { // Prefer dedupe against ACTUAL viewer FOV (resists manual viewer changes) float cur = getViewerFovRadOrNeg(); if (cur > 0.0) { if (llFabs(rad - cur) < gEpsRad) return; // viewer already at this FOV } else { // fallback: dedupe against last value we sent if (gLastSentRad > 0.0 && llFabs(rad - gLastSentRad) < gEpsRad) return; } } gLastSendT = now; gLastSentRad = rad; // RLVa commands are sent via owner chat from attachment scripts. // Many viewers hide "@..." automatically. string cmd = "@" + RLV_FOV_CMD + ":" + (string)rad + "=force"; if (!quiet) { dbg("send " + cmd + " (" + (string)rad2deg(rad) + " deg)"); } llOwnerSay(cmd); } string newReqId() { // unique enough return "FOVSYNC:" + (string)llGetUnixTime() + ":" + (string)((integer)llFrand(1000000.0)); } requestSyncThenMaybeSend(float rad, integer quiet, integer flags) { // optional path: ask Core for state (which can include fov if you patched it), // then decide whether to send. Good for "manual viewer change" corrections gSyncPend = TRUE; gSyncRad = rad; gSyncQuiet = quiet; gSyncFlags = flags; gSyncReq = newReqId(); llMessageLinked(LINK_SET, CE_CMD_GET_STATE, gSyncReq, gOwner); } default { state_entry() { gOwner = llGetOwner(); } on_rez(integer sp) { gOwner = llGetOwner(); gLastSentRad = -1.0; gLastSendT = 0.0; gSyncPend = FALSE; gSyncReq = ""; } attach(key id) { gOwner = llGetOwner(); if (id == NULL_KEY) { // detach cleanup gLastSentRad = -1.0; gLastSendT = 0.0; gSyncPend = FALSE; gSyncReq = ""; } } link_message(integer sender, integer num, string str, key id) { if (num == CE_CMD_RELEASE) { // reset internal state; do NOT spam RLVa on release gLastSentRad = -1.0; gLastSendT = 0.0; gSyncPend = FALSE; gSyncReq = ""; return; } if (num == CE_CMD_FOV) { if (id != gOwner) return; list p = llParseString2List(str, ["|"], []); integer L = llGetListLength(p); if (L < 1) return; float rad = (float)llList2String(p, 0); integer quiet = 1; if (L >= 2) quiet = (integer)llList2String(p, 1); integer flags = 0; if (L >= 3) flags = (integer)llList2String(p, 2); // If you don't want sync at all, just ignore FOVF_SYNC and always send: if (flags & FOVF_SYNC) { requestSyncThenMaybeSend(rad, quiet, flags); } else { sendRlvaFov(rad, quiet, flags); } return; } // optional sync response (only if you patched Core to append fov to CE_EVT_STATE) if (num == CE_EVT_STATE) { if (!gSyncPend) return; list p2 = llParseString2List(str, ["|"], []); if (llGetListLength(p2) < 1) return; string reqId = llList2String(p2, 0); if (reqId != gSyncReq) return; // field 4 (index 4) expected to be fov rad if available float curFov = -1.0; if (llGetListLength(p2) >= 5) { curFov = (float)llList2String(p2, 4); if (curFov > 0.0001) curFov = clampFovRad(curFov); else curFov = -1.0; } // If Core did not provide a usable FOV, force-send to avoid stale cache dedupe if (curFov < 0.0 && !(gSyncFlags & FOVF_FORCE)) { gSyncFlags = gSyncFlags | FOVF_FORCE; } // clear pending first gSyncPend = FALSE; gSyncReq = ""; // if we have current fov, use it for dedupe decision if (curFov > 0.0 && !(gSyncFlags & FOVF_FORCE)) { float want = clampFovRad(gSyncRad); if (llFabs(want - curFov) < gEpsRad) return; } sendRlvaFov(gSyncRad, gSyncQuiet, gSyncFlags); return; } } }