HSDollyCam/HS_CamFov.lsl
2026-05-20 06:03:40 +02:00

220 lines
6.4 KiB
Plaintext

/*
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 (v<lo) return lo; if (v>hi) 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;
}
}
}