Add user manual for HS DollyCam and FOV extension; introduce project context for AI agents
- Created HS DollyCam HUD user manual (v1.3.0) detailing features, setup, commands, and troubleshooting. - Added FOV extension manual outlining FOV commands, usage in playlists, and dollyzoom functionality. - Introduced project context file for AI agents, specifying technology stack, critical implementation rules, and existing patterns.
This commit is contained in:
commit
31443b091f
249
AGENTS.md
Normal file
249
AGENTS.md
Normal file
@ -0,0 +1,249 @@
|
||||
# HS_DollyCam Project Information
|
||||
|
||||
This repository contains LSL (Linden Scripting Language) scripts for **HS_DollyCam**, a professional camera control system for Second Life.
|
||||
|
||||
The project is highly memory-sensitive. LSL script memory is limited, and large lists, mixed-type lists, and repeated string splitting can create avoidable memory pressure. Prefer targeted parsing and short-lived data where practical.
|
||||
|
||||
## Core Components
|
||||
|
||||
The system is split across specialized scripts that communicate mostly via `llMessageLinked`.
|
||||
|
||||
### 1. `HS_CamController.lsl` (Main Controller)
|
||||
|
||||
The central command and state coordinator.
|
||||
|
||||
- Handles chat commands and menu commands.
|
||||
- Coordinates playlist, menu, marker, FOV, and engine scripts.
|
||||
- Saves presets into Linkset Data.
|
||||
- Handles marker click events and forwards camera movement requests.
|
||||
- Maintains FOLLOW and LOCK state in Linkset Data.
|
||||
|
||||
Important note: movement math and continuous tour playback are not owned entirely by the Controller anymore. Controller dispatches movement/tour commands to helper scripts and engine scripts.
|
||||
|
||||
### 2. `HS_CamPlaylist.lsl` (Playlist Manager)
|
||||
|
||||
Primary script for notecard-driven workflows.
|
||||
|
||||
- Reads playlist notecards line by line.
|
||||
- Uses `llGetNotecardLineSync` when the simulator has the notecard cached, with fallback to `llGetNotecardLine` on `NAK`.
|
||||
- Handles `moveto`, `goto`, `load`, `wait`, `tour ... endtour`, `dollyzoom`, `fov`, `lock`, `follow`, and marker show/hide commands.
|
||||
- Builds tour payloads and sends them to `HS_CamEngineTour.lsl` via `CE_CMD_TOUR`.
|
||||
- For notecard tour blocks, the builder stores preset indices, holds, and weights, then loads position/focus only at `endtour`.
|
||||
- Also supports compact notecard tours (`tour <ms> [mode] <idx...>`) to avoid slow multi-line notecard reads when no holds/weights are needed.
|
||||
- Compact notecard tours add an internal `startfirst` mode flag so `HS_CamEngineTour.lsl` starts from the first preset without a current-camera-state roundtrip.
|
||||
- `loadPreset()` intentionally uses targeted field parsing instead of splitting the full preset string into a list.
|
||||
- Standard notecard command parsing uses token helpers rather than full `llParseString2List(line, [" "], [])`.
|
||||
|
||||
The user's primary workflow is Notecard Tours, so optimize this file carefully and prioritize notecard-tour memory use over chat/menu one-liner convenience.
|
||||
|
||||
### 3. `HS_CamTourCommands.lsl` (Secondary Tour Command Helper)
|
||||
|
||||
Dedicated helper for chat/menu one-shot tour builders.
|
||||
|
||||
- Handles `/88 tour ...`, `/88 dollyzoom ...`, and menu-built `TOURRUN` payloads.
|
||||
- Builds `CE_CMD_TOUR` payloads for secondary workflows without adding memory pressure to `HS_CamPlaylist.lsl`.
|
||||
- Uses targeted preset parsing for position/focus/FOV fields.
|
||||
- Keep notecard playlist logic in `HS_CamPlaylist.lsl`; keep chat/menu convenience parsing here.
|
||||
|
||||
### 4. `HS_CamEngineTour.lsl` (Continuous Tour Runtime)
|
||||
|
||||
Dedicated runtime for continuous multi-waypoint camera tours.
|
||||
|
||||
- Receives `CE_CMD_TOUR` payloads from Playlist and TourCommands paths.
|
||||
- Requests current camera state from Core via `CE_CMD_GET_STATE`.
|
||||
- Builds runtime lists for active tour playback: position, focus, holds, segment lengths, cumulative lengths, point times, and optional weights.
|
||||
- Drives the camera by sending `CE_INT_SET_CAM` to `HS_CamEngineCore.lsl`.
|
||||
- Supports spline/linear movement, easing profiles, FOV ramps, and keep-frame mode.
|
||||
- Uses throttled camera-frame sending via `tourSendCam()` to reduce repeated `CE_INT_SET_CAM` messages.
|
||||
- Uses segment caching during timer playback to reduce repeated `llList2Vector` access.
|
||||
- Parses large `CE_CMD_TOUR` payloads using targeted pipe-field helpers rather than splitting the whole payload list.
|
||||
|
||||
Do not move active tour runtime lists into Linkset Data. They are read every timer tick; Linkset Data would reduce script memory but likely hurt runtime performance due to repeated string reads and conversions.
|
||||
|
||||
### 5. `HS_CamEngineCore.lsl` (Camera Engine Core)
|
||||
|
||||
Low-level camera engine.
|
||||
|
||||
- Handles camera permissions, base camera params, MoveTo, Follow, Lock, config, and state queries.
|
||||
- Receives internal tour camera frames with `CE_INT_SET_CAM`.
|
||||
- `CE_INT_SET_CAM` is parsed without `llParseString2List` because it is part of the active tour hot path.
|
||||
- Applies `llSetCameraParams` for active camera movement.
|
||||
- Holds engine configuration and emits config dumps consumed by Playlist and TourEngine.
|
||||
|
||||
Tour-related config keys include:
|
||||
|
||||
- `tour_max_points`
|
||||
- `tour_cam_min_interval`
|
||||
- `tour_pos_epsilon`
|
||||
- `tour_focus_epsilon`
|
||||
|
||||
### 6. `HS_CamMenu.lsl` (Menu/UI)
|
||||
|
||||
Handles dialog menus, page state, menu-driven tour building, nearby target lists, and UI command dispatch.
|
||||
|
||||
- Sends menu commands with `MN_CMD`.
|
||||
- Keeps short UI lists such as nearby avatars/objects and selected tour indices.
|
||||
- Menu paths are secondary compared with notecard tours.
|
||||
|
||||
### 7. `HS_CamMarkers.lsl` (Marker Helper)
|
||||
|
||||
Marker support is a comparatively rare workflow.
|
||||
|
||||
- Rezzes `HS_CamMarker` objects at preset positions.
|
||||
- Uses `MARKER_CH = -880088`.
|
||||
- Uses `llRegionSayTo` for marker setup/cleanup when marker keys are known.
|
||||
- Keeps a mixed marker list internally. This is known to be less memory-efficient, but marker usage is rare and should not be prioritized unless marker workflows become important.
|
||||
|
||||
### 8. `HS_CamFov.lsl`
|
||||
|
||||
Handles Field of View adjustments and FOV-related state synchronization.
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
Primary communication uses `llMessageLinked`.
|
||||
|
||||
Important constants:
|
||||
|
||||
- `MC_CMD = 5100`: Marker helper command channel, e.g. `SHOW|N`, `HIDE`.
|
||||
- `MC_EVT_CLICK = 5101`: Marker click event from Markers to Controller.
|
||||
- `MN_CMD = 7000`: Menu command channel.
|
||||
- `PH_CMD_PLAY = 6100`: Controller/Menu to Playlist play command.
|
||||
- `PH_CMD_STOP = 6101`: Stop playlist command.
|
||||
- `PH_CMD_CHAT_TOUR = 6102`: Chat one-liner tour forwarded to TourCommands.
|
||||
- `PH_CMD_TOURRUN = 6103`: Menu-built tour forwarded to TourCommands.
|
||||
- `PH_CMD_CHAT_DZ = 6104`: Chat DollyZoom forwarded to TourCommands.
|
||||
- `CE_CMD_MOVE = 1010`: Engine move command.
|
||||
- `CE_CMD_TOUR = 1011`: Continuous tour command handled by TourEngine.
|
||||
- `CE_CMD_STOP = 1012`: Stop engine movement.
|
||||
- `CE_CMD_LOCK = 1020`: Lock command.
|
||||
- `CE_CMD_FOLLOW = 1030`: Follow command.
|
||||
- `CE_CMD_FOV = 1040`: FOV command.
|
||||
- `CE_CMD_GET_STATE = 1060`: Request current camera state.
|
||||
- `CE_INT_SET_CAM = 3000`: TourEngine to Core camera frame, payload `<pos>|<focus>`.
|
||||
- `CE_INT_TOUR_BEGIN = 3001`: TourEngine begins external drive.
|
||||
- `CE_INT_TOUR_END = 3002`: TourEngine ends external drive.
|
||||
- `CE_INT_TOUR_STOP = 3003`: Stop external tour drive.
|
||||
- `MARKER_CH = -880088`: Region channel for marker communication.
|
||||
|
||||
## Preset Storage
|
||||
|
||||
Presets are stored in Linkset Data using keys like `P1`, `P2`, etc.
|
||||
|
||||
Preset format:
|
||||
|
||||
```text
|
||||
px|py|pz|fx|fy|fz|rx|ry|rz|rs|fovRad
|
||||
```
|
||||
|
||||
Many consumers only need the first six fields:
|
||||
|
||||
- position: fields `0..2`
|
||||
- focus: fields `3..5`
|
||||
- optional FOV: field `10`
|
||||
|
||||
When reading presets in memory-sensitive code, avoid full `llParseString2List(data, ["|"], [])` if only these fields are needed.
|
||||
|
||||
## Performance And Memory Guidelines
|
||||
|
||||
### Prefer targeted parsing in hot paths
|
||||
|
||||
Avoid full `llParseString2List` when:
|
||||
|
||||
- parsing `CE_INT_SET_CAM`
|
||||
- parsing preset data
|
||||
- parsing large `CE_CMD_TOUR` payloads
|
||||
- parsing normal notecard playlist lines
|
||||
|
||||
Use targeted helpers such as:
|
||||
|
||||
- `lineToken()` in `HS_CamPlaylist.lsl`
|
||||
- pipe-field helpers in `HS_CamEngineTour.lsl`
|
||||
- direct separator lookup for two-field payloads in `HS_CamEngineCore.lsl`
|
||||
|
||||
### Keep active runtime data in script memory
|
||||
|
||||
Active tour playback reads position/focus/segment lists every timer tick. Keep those lists in `HS_CamEngineTour.lsl`; do not replace them with Linkset Data reads.
|
||||
|
||||
Linkset Data is appropriate for persistent presets and shared state, not per-frame runtime data.
|
||||
|
||||
### Minimize mixed-type and large temporary lists
|
||||
|
||||
Avoid mixed-type list builders for large payloads where a direct string build is clear and safe.
|
||||
|
||||
For notecard tour blocks:
|
||||
|
||||
- Store preset indices while parsing.
|
||||
- Load preset position/focus at `endtour`.
|
||||
- Build the final `CE_CMD_TOUR` payload directly.
|
||||
|
||||
### Be careful with timer hot paths
|
||||
|
||||
Timer-driven code should avoid:
|
||||
|
||||
- full string splitting
|
||||
- repeated Linkset Data reads
|
||||
- repeated object detail calls unless needed
|
||||
- unnecessary `llMessageLinked` calls
|
||||
|
||||
Tour playback currently reduces load with:
|
||||
|
||||
- `tour_cam_min_interval`
|
||||
- `tour_pos_epsilon`
|
||||
- `tour_focus_epsilon`
|
||||
- segment caching in TourEngine
|
||||
|
||||
### Use `llRegionSayTo` when target keys are known
|
||||
|
||||
For marker communication, prefer `llRegionSayTo` over `llRegionSay` when the marker key is known. This reduces unnecessary listener wakeups in the region.
|
||||
|
||||
## Configuration
|
||||
|
||||
Engine configuration lives in `HS_CamEngine.properties`.
|
||||
|
||||
Useful current keys:
|
||||
|
||||
```text
|
||||
move_step=0.025
|
||||
follow_step=0.05
|
||||
default_move_ms=3000
|
||||
default_focus_dist=10.0
|
||||
move_pos_lag=0.5
|
||||
move_focus_lag=0.5
|
||||
follow_pos_lag=0.5
|
||||
follow_focus_lag=0.5
|
||||
pos_threshold=0.02
|
||||
focus_threshold=0.02
|
||||
tour_cam_min_interval=0.033
|
||||
tour_pos_epsilon=0.005
|
||||
tour_focus_epsilon=0.005
|
||||
follow_predict=0.10
|
||||
tour_max_points=20
|
||||
```
|
||||
|
||||
`tour_cam_min_interval=0.033` caps internal tour camera updates around 30 Hz while still allowing path computation at `move_step`.
|
||||
|
||||
## Validation Notes
|
||||
|
||||
There is no official local Second Life LSL compiler in this repository. The authoritative compile check is still in-world via the Second Life Viewer or Firestorm.
|
||||
|
||||
Local checks that are useful before in-world compile:
|
||||
|
||||
- brace-balance checks
|
||||
- conflict marker search
|
||||
- targeted `rg` scans for unwanted `llParseString2List` reintroductions in hot paths
|
||||
- optional `lslint` if installed, with the understanding that it is not the official Linden compiler
|
||||
|
||||
## Current Optimization Priorities
|
||||
|
||||
Highest priority:
|
||||
|
||||
- Notecard tour memory behavior.
|
||||
- TourEngine timer hot path.
|
||||
- Core `CE_INT_SET_CAM` hot path.
|
||||
- Large `CE_CMD_TOUR` startup parsing.
|
||||
|
||||
Lower priority:
|
||||
|
||||
- Marker memory layout, because marker use is rare.
|
||||
- Chat one-liner tour convenience paths.
|
||||
- Menu-only tour building paths, unless they become a primary workflow.
|
||||
966
HS_CamController.lsl
Normal file
966
HS_CamController.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
30
HS_CamEngine.properties
Normal file
30
HS_CamEngine.properties
Normal file
@ -0,0 +1,30 @@
|
||||
# --- Ultra-cinematic baseline ---
|
||||
move_step=0.025
|
||||
follow_step=0.05
|
||||
|
||||
default_move_ms=3000
|
||||
default_focus_dist=10.0
|
||||
|
||||
# Viewer smoothing (move)
|
||||
move_pos_lag=0.5
|
||||
move_focus_lag=0.5
|
||||
|
||||
# Viewer smoothing (follow/idle)
|
||||
follow_pos_lag=0.5
|
||||
follow_focus_lag=0.5
|
||||
|
||||
# Deadzone to reduce micro-jitter
|
||||
pos_threshold=0.02
|
||||
focus_threshold=0.02
|
||||
|
||||
# TourEngine internal update throttle.
|
||||
# 0.033 ~= 30 Hz camera updates while still computing the path at move_step.
|
||||
tour_cam_min_interval=0.033
|
||||
tour_pos_epsilon=0.005
|
||||
tour_focus_epsilon=0.005
|
||||
|
||||
# Follow smoothing aid
|
||||
follow_predict=0.10
|
||||
|
||||
# increasing value might cause memory anomalies
|
||||
tour_max_points=20
|
||||
966
HS_CamEngineCore.lsl
Normal file
966
HS_CamEngineCore.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
989
HS_CamEngineTour.lsl
Normal file
989
HS_CamEngineTour.lsl
Normal file
@ -0,0 +1,989 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
966
HS_CamFov.lsl
Normal file
966
HS_CamFov.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
966
HS_CamMarkers.lsl
Normal file
966
HS_CamMarkers.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
966
HS_CamMenu.lsl
Normal file
966
HS_CamMenu.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
966
HS_CamPlaylist.lsl
Normal file
966
HS_CamPlaylist.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
966
HS_CamTourCommands.lsl
Normal file
966
HS_CamTourCommands.lsl
Normal file
@ -0,0 +1,966 @@
|
||||
/*
|
||||
HS_DollyCam - CamController (SLIM)
|
||||
- /88 chat parsing
|
||||
- Presets via Linkset Data (save/load/delete/list)
|
||||
- Playlist player from notecards (one command per line, optional wait <ms>)
|
||||
- TOUR blocks: tour <total_ms> [linear|spline] ... endtour
|
||||
- Config reload/dump
|
||||
- Menu, Playlist, TourCommands and Markers are ROUTED to helper scripts via link_message
|
||||
|
||||
Notes:
|
||||
- idx is valid only if > 0 (slot 0 reserved).
|
||||
- Playlist chains moves on Engine MOVE_DONE unless a wait line appears directly after a moveto (early cut).
|
||||
- Early-cut is disabled for tours (tour is one continuous move).
|
||||
*/
|
||||
|
||||
integer CH = 88;
|
||||
|
||||
// ===== DEMO MODE =====
|
||||
// If DEMO_MODE is TRUE, saving presets is limited to DEMO_MAX_SLOTS (last valid slot = DEMO_MAX_SLOTS)
|
||||
integer DEMO_MODE = FALSE;
|
||||
integer DEMO_MAX_SLOTS = 5;
|
||||
|
||||
// Engine protocol (must match CamEngine)
|
||||
integer CE_CMD_INIT = 1000;
|
||||
integer CE_CMD_RELEASE = 1001;
|
||||
integer CE_CMD_MOVE = 1010;
|
||||
integer CE_CMD_TOUR = 1011; // NEW: continuous multi-waypoint ride
|
||||
integer CE_CMD_STOP = 1012;
|
||||
integer CE_CMD_LOCK = 1020;
|
||||
integer CE_CMD_FOLLOW = 1030;
|
||||
integer CE_CMD_FOV = 1040; // payload: rad|quiet|flags(optional)
|
||||
integer CE_CMD_CFG_RELOAD = 1050;
|
||||
integer CE_CMD_CFG_DUMP = 1051;
|
||||
integer CE_CMD_GET_STATE = 1060;
|
||||
|
||||
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;
|
||||
|
||||
// Helper scripts (separate memory budgets)
|
||||
integer MC_CMD = 5100; // Controller -> Markers script ("SHOW|N"/"HIDE")
|
||||
integer MN_CMD = 5200; // Menu helper -> Controller
|
||||
integer MC_EVT_CLICK = 5101; // Markers script -> Controller (payload: idx)
|
||||
|
||||
// Controller -> Playlist helper
|
||||
integer PH_CMD_PLAY = 6100;
|
||||
integer PH_CMD_STOP = 6101;
|
||||
integer PH_CMD_CHAT_TOUR = 6102;
|
||||
integer PH_CMD_TOURRUN = 6103;
|
||||
integer PH_CMD_CHAT_DZ = 6104; // chat one-liner dollyzoom delegated to TourCommands
|
||||
|
||||
// ===== RLVa FOV =====
|
||||
float RLV_FOV_MIN_DEG = 10.0;
|
||||
float RLV_FOV_MAX_DEG = 179.0; // viewer erlaubt >160; 179 vermeidet “near-180” edge cases
|
||||
|
||||
// Presets
|
||||
string PRE_KEY(integer idx) { return "P" + (string)idx; }
|
||||
|
||||
// Controller runtime
|
||||
key gOwner;
|
||||
integer gListen;
|
||||
|
||||
// Move ids
|
||||
integer gMoveId = 100; // start non-zero
|
||||
integer nextMoveId() { gMoveId++; return gMoveId; }
|
||||
|
||||
// Defaults (updated when engine cfg dump arrives)
|
||||
integer gDefaultMoveMs = 2200;
|
||||
|
||||
|
||||
integer demoSlotOk(integer idx)
|
||||
{
|
||||
if (!DEMO_MODE) return TRUE;
|
||||
|
||||
if (idx <= DEMO_MAX_SLOTS) return TRUE;
|
||||
|
||||
say("!!!DEMO Version !!! limited to max " + (string)DEMO_MAX_SLOTS + " Slots");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// ---- save pending ----
|
||||
integer gSavePending = FALSE;
|
||||
integer gSaveIdx = 0;
|
||||
integer gSaveReq = 0;
|
||||
|
||||
// Temp preset buffer (set by loadPreset)
|
||||
vector gTmpPos;
|
||||
vector gTmpFoc;
|
||||
integer gTmpHasFov = FALSE;
|
||||
float gTmpFovRad = 0.0;
|
||||
|
||||
// “last set by HUD” bleibt als Fallback ok
|
||||
float gLastFovRad = 1.04719755; // ~60°
|
||||
|
||||
// --- Marker menu state (persist across scripts) ---
|
||||
string LSKEY_CAMS = "HS_CAMS"; // "shown|N" e.g. "1|12"
|
||||
|
||||
// ===== ADD: Follow/Lock state persisted for Menu via LinksetData =====
|
||||
// FOLLOW: "on|uuid"
|
||||
// LOCK: "on|arg" (uuid or "<x,y,z>")
|
||||
string LSKEY_FOLLOW = "HS_FOLLOW";
|
||||
string LSKEY_LOCK = "HS_LOCK";
|
||||
|
||||
followLockInitState()
|
||||
{
|
||||
if (llLinksetDataRead(LSKEY_FOLLOW) == "") llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
if (llLinksetDataRead(LSKEY_LOCK) == "") llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
}
|
||||
|
||||
followWrite(integer on, key target)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_FOLLOW, "0|");
|
||||
else llLinksetDataWrite(LSKEY_FOLLOW, "1|" + (string)target);
|
||||
}
|
||||
|
||||
lockWrite(integer on, string arg)
|
||||
{
|
||||
if (!on) llLinksetDataWrite(LSKEY_LOCK, "0|");
|
||||
else llLinksetDataWrite(LSKEY_LOCK, "1|" + arg);
|
||||
}
|
||||
|
||||
// small helpers (keep controller changes localized)
|
||||
engineFollowOff()
|
||||
{
|
||||
// Engine expects at least "on|target"
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
}
|
||||
|
||||
engineLockOff()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
}
|
||||
|
||||
integer gCamsShown = FALSE;
|
||||
integer gCamsN = 12;
|
||||
|
||||
camsLoadState()
|
||||
{
|
||||
string s = llLinksetDataRead(LSKEY_CAMS);
|
||||
if (s == "") {
|
||||
gCamsShown = FALSE; gCamsN = 12;
|
||||
llLinksetDataWrite(LSKEY_CAMS, "0|12");
|
||||
return;
|
||||
}
|
||||
list p = llParseString2List(s, ["|"], []);
|
||||
if (llGetListLength(p) >= 1) gCamsShown = (integer)llList2String(p, 0);
|
||||
if (llGetListLength(p) >= 2) {
|
||||
integer n = (integer)llList2String(p, 1);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 30) n = 30;
|
||||
gCamsN = n;
|
||||
}
|
||||
}
|
||||
|
||||
camsWriteState()
|
||||
{
|
||||
llLinksetDataWrite(LSKEY_CAMS, (string)gCamsShown + "|" + (string)gCamsN);
|
||||
}
|
||||
|
||||
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 = rad2deg(rad);
|
||||
deg = clampf(deg, RLV_FOV_MIN_DEG, RLV_FOV_MAX_DEG);
|
||||
return deg2rad(deg);
|
||||
}
|
||||
|
||||
string presetDescribe(integer idx, string data)
|
||||
{
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
integer L = llGetListLength(p);
|
||||
if (L < 6) return "Preset " + (string)idx + " = (corrupt/too short)";
|
||||
|
||||
vector pos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
vector foc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
string s = "Preset " + (string)idx + " pos=" + (string)pos + " foc=" + (string)foc;
|
||||
|
||||
// v2: optional fovRad at index 10
|
||||
if (L >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) {
|
||||
fr = clampFovRad(fr);
|
||||
float deg = rad2deg(fr);
|
||||
s += " fov=" + fmtFloat(deg) + "° (" + fmtFloat(fr) + "rad)";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s += " fov=(none)";
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== RLVa helpers =====
|
||||
float deg2rad(float deg) { return deg * PI / 180.0; }
|
||||
float rad2deg(float rad) { return rad * 180.0 / PI; }
|
||||
|
||||
string fmtFloat(float v)
|
||||
{
|
||||
// LSL string(float) is fine; this is just to keep logs readable
|
||||
string s = (string)v;
|
||||
if (llStringLength(s) > 10) s = llGetSubString(s, 0, 9);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
integer isValidIdx(integer idx) { return (idx > 0); }
|
||||
|
||||
say(string s) { llOwnerSay(s); }
|
||||
|
||||
// Playlist / TourCommand helpers
|
||||
phStop(string reason)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_STOP, reason, gOwner);
|
||||
}
|
||||
|
||||
phPlay(string card, integer gapMs)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_PLAY, card + "|" + (string)gapMs, gOwner);
|
||||
}
|
||||
|
||||
phChatTour(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_TOUR, line, gOwner);
|
||||
}
|
||||
|
||||
phMenuTourRun(string raw)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_TOURRUN, raw, gOwner);
|
||||
}
|
||||
|
||||
phChatDollyZoom(string line)
|
||||
{
|
||||
llMessageLinked(LINK_SET, PH_CMD_CHAT_DZ, line, gOwner);
|
||||
}
|
||||
|
||||
// Single-prim HUD (Controller in ROOT)
|
||||
hudHide()
|
||||
{
|
||||
llSetAlpha(0.0, ALL_SIDES);
|
||||
}
|
||||
hudShow()
|
||||
{
|
||||
llSetAlpha(1.0, ALL_SIDES);
|
||||
}
|
||||
|
||||
|
||||
// ---------- engine commands ----------
|
||||
engineInit()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_INIT, "", gOwner);
|
||||
}
|
||||
|
||||
engineRelease()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_RELEASE, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
integer engineMove(vector pos, vector foc, integer durMs)
|
||||
{
|
||||
integer mid = nextMoveId();
|
||||
string payload = (string)mid + "|" + (string)durMs + "|" + (string)pos + "|" + (string)foc + "|src=CTRL";
|
||||
llMessageLinked(LINK_SET, CE_CMD_MOVE, payload, gOwner);
|
||||
|
||||
// Hide HUD during non-instant moves (avoid flicker on cuts)
|
||||
if (durMs > 0) hudHide();
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
engineStopMove()
|
||||
{
|
||||
llMessageLinked(LINK_SET, CE_CMD_STOP, "src=CTRL", gOwner);
|
||||
}
|
||||
|
||||
engineCfgReload() { llMessageLinked(LINK_SET, CE_CMD_CFG_RELOAD, "", gOwner); }
|
||||
engineCfgDump() { llMessageLinked(LINK_SET, CE_CMD_CFG_DUMP, "", gOwner); }
|
||||
|
||||
// ---------- presets ----------
|
||||
string packPreset(vector pos, vector foc, rotation rot, float fovRad)
|
||||
{
|
||||
// v2 preset format: + fovRad at the end (index 10)
|
||||
return llDumpList2String([
|
||||
(string)pos.x,(string)pos.y,(string)pos.z,
|
||||
(string)foc.x,(string)foc.y,(string)foc.z,
|
||||
(string)rot.x,(string)rot.y,(string)rot.z,(string)rot.s,
|
||||
(string)fovRad
|
||||
], "|");
|
||||
}
|
||||
|
||||
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 (we only need first 6 here)
|
||||
list p = llParseString2List(data, ["|"], []);
|
||||
if (llGetListLength(p) < 6) return FALSE;
|
||||
|
||||
gTmpPos = <(float)llList2String(p,0),(float)llList2String(p,1),(float)llList2String(p,2)>;
|
||||
gTmpFoc = <(float)llList2String(p,3),(float)llList2String(p,4),(float)llList2String(p,5)>;
|
||||
|
||||
gTmpHasFov = FALSE;
|
||||
gTmpFovRad = 0.0;
|
||||
if (llGetListLength(p) >= 11) {
|
||||
float fr = (float)llList2String(p, 10);
|
||||
if (fr > 0.0001) { gTmpHasFov = TRUE; gTmpFovRad = clampFovRad(fr); }
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
applyLoadedPresetFov()
|
||||
{
|
||||
if (!gTmpHasFov) return;
|
||||
|
||||
gLastFovRad = gTmpFovRad; // keep save fallback in sync
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)gTmpFovRad + "|1", gOwner);
|
||||
}
|
||||
|
||||
// ---------- follow parsing helpers ----------
|
||||
integer FOLLOW_YAW = 0;
|
||||
integer FOLLOW_LOCAL = 1;
|
||||
integer FOLLOW_WORLD = 2;
|
||||
|
||||
integer followModeFrom(string s)
|
||||
{
|
||||
s = llToLower(s);
|
||||
if (s == "yaw") return FOLLOW_YAW;
|
||||
if (s == "local") return FOLLOW_LOCAL;
|
||||
return FOLLOW_WORLD;
|
||||
}
|
||||
|
||||
// Re-join tokens that represent a <vector> that may contain spaces.
|
||||
// returns [string joined, integer nextIndex]
|
||||
list takeAngleToken(list toks, integer i)
|
||||
{
|
||||
integer n = llGetListLength(toks);
|
||||
if (i >= n) return ["", i];
|
||||
|
||||
string s = llList2String(toks, i);
|
||||
|
||||
if (llGetSubString(s, 0, 0) != "<") {
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
while (i + 1 < n && llGetSubString(s, -1, -1) != ">") {
|
||||
++i;
|
||||
s += " " + llList2String(toks, i);
|
||||
}
|
||||
return [s, i + 1];
|
||||
}
|
||||
|
||||
// ---------- chat commands ----------
|
||||
printHelp()
|
||||
{
|
||||
say(
|
||||
"HS DollyCam — Commands (/88)\n"
|
||||
+ "/88 help\n"
|
||||
+ "/88 cam on|off\n"
|
||||
+ "/88 save <idx>\n"
|
||||
+ "/88 load <idx> (cut)\n"
|
||||
+ "/88 moveto <idx> [ms]\n"
|
||||
+ "/88 del <idx>\n"
|
||||
+ "/88 list [from] [count]\n"
|
||||
+ "/88 play <notecard> [gap_ms]\n"
|
||||
+ "/88 stop\n"
|
||||
+ "/88 tour <ms> [mode] <idx1> <idx2> ...\n"
|
||||
+ "/88 cfg reload|dump\n"
|
||||
+ "/88 show cams [N]\n"
|
||||
+ "/88 hide cams\n"
|
||||
+ "/88 lock on [<x,y,z>|uuid]\n"
|
||||
+ "/88 lock off\n"
|
||||
+ "/88 follow on [uuid] [yaw|local|world] [transition_ms]\n"
|
||||
+ "/88 follow off\n"
|
||||
+ "/88 fov <rad> (sets viewer FOV via RLVa; rad ~ 1.0472 for 60°)\n"
|
||||
+ "/88 fovdeg <deg> (sets viewer FOV via RLVa; deg 10..179)\n"
|
||||
+ "/88 dollyzoom <ms> [mode] <idxA> <idxB>\n"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- default ----------
|
||||
default
|
||||
{
|
||||
state_entry()
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
camsLoadState();
|
||||
followLockInitState(); // NEW: ensure menu keys exist
|
||||
hudShow();
|
||||
gListen = llListen(CH, "", "", "");
|
||||
|
||||
say("HS DollyCam Controller (slim) ready. Type /88 help");
|
||||
engineCfgDump();
|
||||
|
||||
// AUTO CAM ON when script starts while worn
|
||||
if (llGetAttached() != 0)
|
||||
engineInit();
|
||||
}
|
||||
|
||||
on_rez(integer sp)
|
||||
{
|
||||
gOwner = llGetOwner();
|
||||
}
|
||||
|
||||
attach(key id)
|
||||
{
|
||||
if (id == NULL_KEY) {
|
||||
phStop("HUD detached.");
|
||||
hudShow();
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
|
||||
// keep menu toggles consistent after detach
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
lockWrite(FALSE, "");
|
||||
|
||||
// IMPORTANT:
|
||||
// Do NOT also send MC_CMD/HM_CMD here.
|
||||
// Markers/Manual scripts should clean up in their own attach(NULL_KEY),
|
||||
// otherwise you'll get double cleanup logs.
|
||||
// llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
// llMessageLinked(LINK_SET, HM_CMD, "STOP", gOwner);
|
||||
|
||||
engineRelease();
|
||||
} else {
|
||||
gOwner = llGetOwner();
|
||||
hudShow();
|
||||
engineInit();
|
||||
engineCfgDump();
|
||||
}
|
||||
}
|
||||
|
||||
link_message(integer sender, integer num, string str, key id)
|
||||
{
|
||||
if (num == CE_EVT_READY) {
|
||||
say("Camera control granted.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_DENIED) {
|
||||
say("Camera permission denied.");
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_CFG_DUMP) {
|
||||
list kv = llParseString2List(str, ["|"], []);
|
||||
integer i;
|
||||
say("Engine cfg:");
|
||||
for (i=0; i<llGetListLength(kv); ++i) {
|
||||
string pair = llList2String(kv,i);
|
||||
integer eq = llSubStringIndex(pair, "=");
|
||||
if (eq < 1) jump next;
|
||||
string k = llToLower(llGetSubString(pair, 0, eq-1));
|
||||
string v = llGetSubString(pair, eq+1, -1);
|
||||
|
||||
say(" " + k + "=" + v);
|
||||
|
||||
if (k == "default_move_ms") gDefaultMoveMs = (integer)v;
|
||||
@next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_MOVE_DONE ) {
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
if (num == CE_EVT_STATE) {
|
||||
// payload: reqId|<pos>|<focus>|<rot>
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
if (llGetListLength(p) < 4) return;
|
||||
integer req = (integer)llList2String(p,0);
|
||||
|
||||
if (gSavePending && req == gSaveReq) {
|
||||
vector pos = (vector)llList2String(p,1);
|
||||
vector foc = (vector)llList2String(p,2);
|
||||
|
||||
rotation rot = (rotation)llList2String(p,3);
|
||||
|
||||
// fovRad: aus CE_EVT_STATE (falls vorhanden), sonst fallback
|
||||
float fovRad = gLastFovRad;
|
||||
if (llGetListLength(p) >= 5) {
|
||||
float got = (float)llList2String(p, 4);
|
||||
if (got > 0.0001) fovRad = got;
|
||||
}
|
||||
fovRad = clampFovRad(fovRad);
|
||||
|
||||
string data = packPreset(pos, foc, rot, fovRad);
|
||||
llLinksetDataWrite(PRE_KEY(gSaveIdx), data);
|
||||
|
||||
// volle Anzeige:
|
||||
say("Saved " + presetDescribe(gSaveIdx, data));
|
||||
|
||||
gSavePending = FALSE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker click event from HS_CamMarkers.lsl
|
||||
if (num == MC_EVT_CLICK) {
|
||||
if (id != gOwner) return;
|
||||
|
||||
integer idx = (integer)str;
|
||||
if (!isValidIdx(idx)) return;
|
||||
|
||||
if (!loadPreset(idx)) { say("Marker click: preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by marker click.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, gDefaultMoveMs);
|
||||
say("Loaded via marker: " + (string)idx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Menu commands from HS_CamMenu.lsl
|
||||
if (num == MN_CMD) {
|
||||
if (id != gOwner) return; // only trust owner-routed messages
|
||||
|
||||
list p = llParseString2List(str, ["|"], []);
|
||||
integer len = llGetListLength(p);
|
||||
if (len < 1) return;
|
||||
|
||||
string typ = llToUpper(llList2String(p, 0));
|
||||
// ===== ADD: FOLLOW/LOCK from Menu (place BEFORE other returns) =====
|
||||
if (typ == "FOLLOW" && len >= 2) {
|
||||
string actF = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actF == "OFF") {
|
||||
engineFollowOff();
|
||||
say("Follow OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actF == "ON" && len >= 3) {
|
||||
key tgtF = (key)llList2String(p, 2);
|
||||
if (tgtF == NULL_KEY) { say("Follow: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineLockOff();
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 250;
|
||||
|
||||
string payload =
|
||||
"1|" + (string)tgtF + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, tgtF);
|
||||
|
||||
say("Follow ON -> " + llKey2Name(tgtF));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typ == "LOCK" && len >= 2) {
|
||||
string actL = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (actL == "OFF") {
|
||||
engineLockOff();
|
||||
say("Lock OFF (menu)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (actL == "ON" && len >= 3) {
|
||||
key tgtL = (key)llList2String(p, 2);
|
||||
if (tgtL == NULL_KEY) { say("Lock: invalid target."); return; }
|
||||
|
||||
// mutual exclusion
|
||||
engineFollowOff();
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)tgtL, gOwner);
|
||||
lockWrite(TRUE, (string)tgtL);
|
||||
|
||||
say("Lock ON -> " + llKey2Name(tgtL));
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SAVE|idx
|
||||
if (typ == "SAVE" && len >= 2) {
|
||||
integer idx = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by menu save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
// MOVETO|idx|ms
|
||||
if (typ == "MOVETO" && len >= 3) {
|
||||
integer idx2 = (integer)llList2String(p, 1);
|
||||
integer ms = (integer)llList2String(p, 2);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx2)) { say("Preset not found."); return; }
|
||||
|
||||
if (ms < 1) ms = gDefaultMoveMs; // menu can send 0 => use default
|
||||
phStop("Interrupted by menu moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx2 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
// LOAD|idx (optional: menu supports cut)
|
||||
if (typ == "LOAD" && len >= 2) {
|
||||
integer idx3 = (integer)llList2String(p, 1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
phStop("Interrupted by menu load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
// PLAY|card|gap
|
||||
if (typ == "PLAY" && len >= 2) {
|
||||
string card = llList2String(p, 1);
|
||||
integer gap = 0;
|
||||
if (len >= 3) gap = (integer)llList2String(p, 2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// STOP
|
||||
if (typ == "STOP") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// TOURRUN|totalMs|mode|count|idx1|idx2|...
|
||||
if (typ == "TOURRUN") {
|
||||
phStop("Interrupted by menu tour.");
|
||||
phMenuTourRun(str);
|
||||
return;
|
||||
}
|
||||
|
||||
// CAMS|SHOW|N or CAMS|HIDE
|
||||
if (typ == "CAMS" && len >= 2) {
|
||||
string act = llToUpper(llList2String(p, 1));
|
||||
|
||||
if (act == "SHOW") {
|
||||
integer want = gCamsN;
|
||||
if (len >= 3) want = (integer)llList2String(p, 2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (act == "HIDE") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string msg)
|
||||
{
|
||||
// Accept commands from: my avatar OR any object owned by me (including attachments)
|
||||
if (llGetOwnerKey(id) != gOwner) return;
|
||||
|
||||
msg = llStringTrim(msg, STRING_TRIM);
|
||||
if (msg == "") return;
|
||||
|
||||
list t = llParseString2List(msg, [" "], []);
|
||||
integer n = llGetListLength(t);
|
||||
string cmd = llToLower(llList2String(t,0));
|
||||
|
||||
if (cmd == "help") { printHelp(); return; }
|
||||
|
||||
if (cmd == "cam" && n >= 2) {
|
||||
string sw = llToLower(llList2String(t,1));
|
||||
if (sw == "on") engineInit();
|
||||
else engineRelease();
|
||||
hudShow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "cfg" && n >= 2) {
|
||||
string sub = llToLower(llList2String(t,1));
|
||||
if (sub == "reload") engineCfgReload();
|
||||
else engineCfgDump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "save" && n >= 2) {
|
||||
integer idx = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx)) { say("idx must be > 0"); return; }
|
||||
|
||||
// DEMO limit
|
||||
if (!demoSlotOk(idx)) return;
|
||||
|
||||
phStop("Interrupted by save.");
|
||||
gSavePending = TRUE;
|
||||
gSaveIdx = idx;
|
||||
gSaveReq++;
|
||||
llMessageLinked(LINK_SET, CE_CMD_GET_STATE, (string)gSaveReq, gOwner);
|
||||
say("Saving preset " + (string)idx + " ...");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((cmd == "del" || cmd == "delete") && n >= 2) {
|
||||
integer idx2 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx2)) { say("idx must be > 0"); return; }
|
||||
llLinksetDataDelete(PRE_KEY(idx2));
|
||||
say("Deleted preset " + (string)idx2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "load" && n >= 2) {
|
||||
integer idx3 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx3)) { say("idx must be > 0"); return; }
|
||||
|
||||
if (!loadPreset(idx3)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by load.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, 0);
|
||||
say("Loaded preset " + (string)idx3 + " (cut)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "moveto" && n >= 2) {
|
||||
integer idx4 = (integer)llList2String(t,1);
|
||||
if (!isValidIdx(idx4)) { say("idx must be > 0"); return; }
|
||||
|
||||
integer ms = gDefaultMoveMs;
|
||||
if (n >= 3) ms = (integer)llList2String(t,2);
|
||||
|
||||
if (!loadPreset(idx4)) { say("Preset not found."); return; }
|
||||
|
||||
phStop("Interrupted by moveto.");
|
||||
engineStopMove();
|
||||
applyLoadedPresetFov();
|
||||
engineMove(gTmpPos, gTmpFoc, ms);
|
||||
say("MoveTo preset " + (string)idx4 + " (" + (string)ms + "ms)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "stop") {
|
||||
hudShow();
|
||||
phStop("User stop.");
|
||||
engineStopMove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "play" && n >= 2) {
|
||||
string card = llList2String(t,1);
|
||||
integer gap = 0;
|
||||
if (n >= 3) gap = (integer)llList2String(t,2);
|
||||
phPlay(card, gap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner Tour:
|
||||
// /88 tour <total_ms> [mode] <idx1> <idx2> ... <idxN>
|
||||
if (cmd == "tour") {
|
||||
// Delegate heavy parsing/building to playlist helper (one-shot tour)
|
||||
phStop("Interrupted by tour (chat).");
|
||||
phChatTour(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat one-liner DollyZoom:
|
||||
// /88 dollyzoom <total_ms> [mode] <idxA> <idxB> [keepframe? optional later]
|
||||
if (cmd == "dollyzoom") {
|
||||
phStop("Interrupted by dollyzoom (chat).");
|
||||
phChatDollyZoom(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
integer from = 1;
|
||||
integer count = 20;
|
||||
if (n >= 2) from = (integer)llList2String(t,1);
|
||||
if (n >= 3) count = (integer)llList2String(t,2);
|
||||
if (from < 1) from = 1;
|
||||
|
||||
integer shown = 0;
|
||||
integer i;
|
||||
for (i = from; i <= 999 && shown < count; ++i) {
|
||||
string data = llLinksetDataRead(PRE_KEY(i));
|
||||
if (data != "") {
|
||||
say(presetDescribe(i, data));
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
if (!shown) say("No presets found in range.");
|
||||
return;
|
||||
}
|
||||
|
||||
// markers routed to helper script
|
||||
if (cmd == "show" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
integer want = 12;
|
||||
if (n >= 3) want = (integer)llList2String(t,2);
|
||||
if (want < 1) want = 1;
|
||||
if (want > 30) want = 30;
|
||||
|
||||
llMessageLinked(LINK_SET, MC_CMD, "SHOW|" + (string)want, gOwner);
|
||||
|
||||
gCamsShown = TRUE;
|
||||
gCamsN = want;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "hide" && n >= 2 && llToLower(llList2String(t,1)) == "cams") {
|
||||
llMessageLinked(LINK_SET, MC_CMD, "HIDE", gOwner);
|
||||
|
||||
gCamsShown = FALSE;
|
||||
camsWriteState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "lock" && n >= 2) {
|
||||
string sw3 = llToLower(llList2String(t,1));
|
||||
integer on3 = (sw3 == "on" || sw3 == "1" || sw3 == "true");
|
||||
|
||||
if (!on3) {
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "0|<0,0,0>", gOwner);
|
||||
lockWrite(FALSE, "");
|
||||
say("Lock OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Lock ON disables Follow
|
||||
engineFollowOff();
|
||||
|
||||
string lockArgUsed = "<0,0,0>";
|
||||
|
||||
if (n >= 3) {
|
||||
list r = takeAngleToken(t, 2);
|
||||
string arg = llList2String(r, 0);
|
||||
|
||||
if (llGetSubString(arg,0,0) == "<") {
|
||||
lockArgUsed = arg;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + arg, gOwner);
|
||||
} else {
|
||||
key k = (key)arg;
|
||||
if (k != NULL_KEY) {
|
||||
lockArgUsed = (string)k;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)k, gOwner);
|
||||
} else {
|
||||
vector camPos = llGetCameraPos();
|
||||
rotation camRot = llGetCameraRot();
|
||||
vector foc2 = camPos + (llRot2Fwd(camRot) * 10.0);
|
||||
lockArgUsed = (string)foc2;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc2, gOwner);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vector camPos2 = llGetCameraPos();
|
||||
rotation camRot2 = llGetCameraRot();
|
||||
vector foc3 = camPos2 + (llRot2Fwd(camRot2) * 10.0);
|
||||
lockArgUsed = (string)foc3;
|
||||
llMessageLinked(LINK_SET, CE_CMD_LOCK, "1|" + (string)foc3, gOwner);
|
||||
}
|
||||
|
||||
lockWrite(TRUE, lockArgUsed);
|
||||
say("Lock ON");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "follow" && n >= 2) {
|
||||
string sw4 = llToLower(llList2String(t,1));
|
||||
integer on4 = (sw4 == "on" || sw4 == "1" || sw4 == "true");
|
||||
|
||||
key target = gOwner;
|
||||
if (n >= 3) target = (key)llList2String(t,2);
|
||||
|
||||
integer mode = FOLLOW_WORLD;
|
||||
integer trans = 0;
|
||||
|
||||
if (n >= 4) mode = followModeFrom(llList2String(t,3));
|
||||
if (n >= 5) trans = (integer)llList2String(t,4);
|
||||
if (trans < 0) trans = 0;
|
||||
|
||||
if (!on4) {
|
||||
// OFF: do not touch lock
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, "0|" + (string)gOwner, gOwner);
|
||||
followWrite(FALSE, NULL_KEY);
|
||||
say("Follow OFF");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion: Follow ON disables Lock
|
||||
engineLockOff();
|
||||
|
||||
// capture-follow: offsets ZERO, engine captures
|
||||
string payload =
|
||||
"1|" + (string)target + "|" + (string)ZERO_VECTOR + "|" + (string)ZERO_VECTOR
|
||||
+ "|" + (string)mode + "|" + (string)trans;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOLLOW, payload, gOwner);
|
||||
followWrite(TRUE, target);
|
||||
|
||||
say("Follow ON mode=" + (string)mode + " trans=" + (string)trans);
|
||||
return;
|
||||
}
|
||||
|
||||
// /88 fov <rad> (or degrees if >3.2)
|
||||
if (cmd == "fov" && n >= 2) {
|
||||
float v = (float)llList2String(t, 1);
|
||||
|
||||
// Heuristic: if user typed "60", it's likely degrees
|
||||
float rad = v;
|
||||
if (v > 3.2) rad = v * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
// quiet=0 for manual commands, flags=1 (sync)
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "fovdeg" && n >= 2) {
|
||||
float deg = (float)llList2String(t, 1);
|
||||
float rad = deg * PI / 180.0;
|
||||
|
||||
rad = clampFovRad(rad);
|
||||
gLastFovRad = rad;
|
||||
|
||||
llMessageLinked(LINK_SET, CE_CMD_FOV, (string)rad + "|0|1", gOwner);
|
||||
return;
|
||||
}
|
||||
|
||||
say("Unknown command. /88 help");
|
||||
}
|
||||
}
|
||||
506
HS_DollyCam_Manual
Normal file
506
HS_DollyCam_Manual
Normal file
@ -0,0 +1,506 @@
|
||||
HS DollyCam HUD - User Manual (v1.3.0)
|
||||
====================================
|
||||
|
||||
1) What this HUD does
|
||||
---------------------
|
||||
HS DollyCam is a cinematic camera dolly tool for Second Life.
|
||||
|
||||
It lets you:
|
||||
- Save camera presets (position + focus)
|
||||
- Move smoothly between presets (moveto) or cut instantly (load)
|
||||
- Play camera playlists from notecards (one command per line)
|
||||
- Use TOUR mode for one continuous camera ride across waypoints
|
||||
- Use FOV presets, FOV ramps and DollyZoom via RLVa-enabled viewers
|
||||
- Use optional lock/follow modes (secondary, lower priority than moves)
|
||||
- Visualize presets with clickable marker "pyramids" (optional helper)
|
||||
- Use the touch menu for common Save / MoveTo / Play / Tour / Follow / Lock workflows
|
||||
|
||||
2) Requirements & Permissions
|
||||
-----------------------------
|
||||
Camera permissions:
|
||||
- The HUD requests Camera Control permission (viewer popup).
|
||||
- It also requests Camera Tracking for better capture/saving accuracy.
|
||||
|
||||
FOV / DollyZoom:
|
||||
- FOV commands require a viewer with RLVa support enabled.
|
||||
- If RLVa is off, normal camera motion still works, but FOV changes do nothing.
|
||||
|
||||
Markers ("show cams"):
|
||||
- Parcel must allow you to rez objects (or allow attachments to rez).
|
||||
- Parcel must have enough remaining object capacity / LI.
|
||||
- Markers will fail on no-rez parcels or if you hit parcel object limits.
|
||||
|
||||
3) Setup
|
||||
--------
|
||||
1) Wear the HUD.
|
||||
2) The HUD automatically asks for camera control when it starts while worn.
|
||||
You can also enable camera control manually:
|
||||
|
||||
/88 cam on
|
||||
|
||||
Tip (Viewer quirk / SL restriction):
|
||||
Sometimes the viewer does not immediately switch into the scripted camera mode,
|
||||
especially if you recently moved/rotated the camera manually or a UI element
|
||||
has input focus. If the dolly cam feels like it is "not taking over", press
|
||||
ESC once (sometimes twice).
|
||||
|
||||
Why this happens:
|
||||
This is a Second Life viewer limitation. ESC clears certain viewer-side camera
|
||||
and input states (camera manipulation focus, mouselook, modal UI capture).
|
||||
The HUD can set camera parameters only when the viewer is ready to accept them,
|
||||
so ESC often nudges the viewer back into the correct state.
|
||||
|
||||
3) Accept the permission popup.
|
||||
|
||||
To release camera control later:
|
||||
|
||||
/88 cam off
|
||||
|
||||
|
||||
4) Quick Start (60 seconds)
|
||||
---------------------------
|
||||
1) Move your viewer camera to a nice framing.
|
||||
|
||||
/88 save 1
|
||||
|
||||
2) Aim for another framing.
|
||||
|
||||
/88 save 2
|
||||
|
||||
3) Smooth cinematic moves:
|
||||
|
||||
/88 moveto 1 2500
|
||||
/88 moveto 2 2500
|
||||
|
||||
4) Instant cut:
|
||||
|
||||
/88 load 1
|
||||
|
||||
5) Chat Commands (/88)
|
||||
----------------------
|
||||
|
||||
A) Help
|
||||
/88 help
|
||||
Shows a quick cheat sheet.
|
||||
|
||||
B) Camera
|
||||
/88 cam on
|
||||
Requests permission and activates dolly camera.
|
||||
|
||||
/88 cam off
|
||||
Releases camera and clears camera params.
|
||||
|
||||
C) Presets (Linkset Data)
|
||||
/88 save <idx>
|
||||
Saves current camera position + focus to preset slot <idx>.
|
||||
|
||||
/88 load <idx>
|
||||
Instant cut to preset <idx> (no smoothing).
|
||||
|
||||
/88 moveto <idx> [duration_ms]
|
||||
Smooth move to preset <idx> over duration_ms.
|
||||
If duration_ms is omitted, default_move_ms is used.
|
||||
|
||||
/88 del <idx>
|
||||
Deletes preset <idx>.
|
||||
|
||||
/88 list [from] [count]
|
||||
Lists saved presets in a range.
|
||||
|
||||
Important:
|
||||
- preset index must be > 0 (slot 0 is reserved).
|
||||
|
||||
D) Playlist
|
||||
/88 play <notecard> [gap_ms]
|
||||
Plays a playlist notecard (one command per line).
|
||||
gap_ms adds a delay after each action (default 0).
|
||||
|
||||
/88 stop
|
||||
Stops playlist and stops the current move.
|
||||
|
||||
E) Config
|
||||
/88 cfg reload
|
||||
Reload engine config from HS_CamEngine.properties.
|
||||
|
||||
/88 cfg dump
|
||||
Prints current engine config values.
|
||||
|
||||
F) Markers (requires HS_CamMarkers helper)
|
||||
/88 show cams [N]
|
||||
Rezzes up to N clickable markers for saved presets.
|
||||
|
||||
/88 hide cams
|
||||
Cleans up all markers created by the HUD.
|
||||
|
||||
G) Secondary modes (lower priority than MoveTo / Tour)
|
||||
LOCK (focus only):
|
||||
/88 lock on [<x,y,z>|uuid]
|
||||
/88 lock off
|
||||
|
||||
FOLLOW (capture follow):
|
||||
/88 follow on [uuid] [yaw|local|world] [transition_ms]
|
||||
/88 follow off
|
||||
|
||||
H) FOV / DollyZoom (requires RLVa)
|
||||
Note: one-line /88 tour, /88 dollyzoom and menu Tour Builder use
|
||||
HS_CamTourCommands.lsl.
|
||||
|
||||
/88 fov <rad>
|
||||
Sets viewer FOV in radians. Values greater than 3.2 are treated as degrees.
|
||||
|
||||
/88 fovdeg <deg>
|
||||
Sets viewer FOV in degrees. Range is clamped to 10..179.
|
||||
|
||||
/88 tour <ms> [mode] <idx1> <idx2> ... [fovdeg <a> <b>|fov <radA> <radB>]
|
||||
Runs a one-line tour and optionally ramps FOV from a to b.
|
||||
|
||||
/88 dollyzoom <ms> [mode] <idxA> <idxB> [keep]
|
||||
Runs a 2-point tour using stored FOV from the presets. "keep" computes
|
||||
FOV from camera distance to keep framing more stable.
|
||||
|
||||
Tip:
|
||||
For reliable DollyZoom results, set FOV explicitly before saving both
|
||||
presets, e.g. "/88 fovdeg 35" then "/88 save 1", then set the second
|
||||
FOV and "/88 save 2".
|
||||
|
||||
I) Touch menu
|
||||
Touch the HUD to open the menu.
|
||||
The menu supports Save, MoveTo/Cut, Play, Tour Builder, Settings,
|
||||
Follow/Lock target picking, Show/Hide Cams and Stop.
|
||||
|
||||
Notes:
|
||||
- The menu shows preset slots 1..30.
|
||||
- The Play menu shows only notecards whose names start with "shot_".
|
||||
- Chat /88 play can still play any notecard name you provide.
|
||||
|
||||
6) Playlist Notecard Format
|
||||
---------------------------
|
||||
Create a notecard, put it into the HUD contents, then:
|
||||
|
||||
/88 play MyPlaylist 250
|
||||
|
||||
The playlist reader uses the simulator notecard cache when available, with an
|
||||
automatic fallback to normal dataserver reads.
|
||||
|
||||
Rules:
|
||||
- One command per line (NO "/88" prefix in notecards).
|
||||
- Empty lines and comments are allowed:
|
||||
# comment
|
||||
// comment
|
||||
; comment
|
||||
- Tokens are split by spaces. (The playlist parser does NOT re-join vectors that contain spaces.)
|
||||
|
||||
Supported playlist commands (core):
|
||||
- moveto <idx> [ms]
|
||||
- goto <idx> [ms] (alias of moveto)
|
||||
- load <idx> [ms] (default is cut; if [ms] is provided it will be used)
|
||||
- wait <ms>
|
||||
- fov <rad> [quiet]
|
||||
- fovdeg <deg> [quiet]
|
||||
- dollyzoom <ms> [mode] <idxA> <idxB> [keep]
|
||||
- lock on|off [arg]
|
||||
- follow on|off [target] [modeInt] [transition_ms]
|
||||
- tour ... / endtour
|
||||
- show cams [N] / hide cams
|
||||
|
||||
Basic example:
|
||||
# My cinematic playlist
|
||||
moveto 1 2500
|
||||
wait 300
|
||||
moveto 2 2200
|
||||
wait 150
|
||||
load 3
|
||||
moveto 4 3000
|
||||
|
||||
Notes / pitfalls (playlist):
|
||||
- "load <idx>" is a cut by default, but "load <idx> <ms>" will move smoothly (same as moveto with that duration).
|
||||
- For lock vectors in notecards, write vectors without spaces, e.g. <128,128,30>.
|
||||
(Vectors with spaces like <128, 128, 30> are NOT supported in playlists.)
|
||||
- For follow in notecards, mode is an integer: 0=yaw, 1=local, 2=world.
|
||||
Words like "yaw/local/world" are not parsed as words in playlists.
|
||||
- For weighted tours, waypoint lines can include weight tokens such as
|
||||
"w=1.5" or speed tokens such as "speed=2.0".
|
||||
- Notecard tour blocks are the primary optimized workflow. Internally the HUD
|
||||
stores only preset indices while reading the block, then loads position/focus
|
||||
data at endtour.
|
||||
|
||||
7) Playlist Timing: MOVE_DONE chaining + "early cut"
|
||||
----------------------------------------------------
|
||||
Normal behavior:
|
||||
- After a move command (moveto/goto/load), the playlist normally continues when the engine signals MOVE_DONE.
|
||||
|
||||
Early cut rule (timing):
|
||||
- If the NEXT actionable line (ignoring empty/comment lines) after a move command is:
|
||||
|
||||
wait <ms>
|
||||
|
||||
...the playlist will "early cut":
|
||||
- it starts that wait immediately,
|
||||
- then continues after the wait ends,
|
||||
- even if the move has not fully finished.
|
||||
|
||||
Notes:
|
||||
- Early cut applies after moveto, goto, and load.
|
||||
- Early cut is automatically DISABLED for Tours (a Tour is one continuous move).
|
||||
- gap_ms (from "/88 play <card> [gap_ms]") is applied after MOVE_DONE; if early cut is used,
|
||||
the playlist no longer waits for MOVE_DONE, so the gap may not be applied.
|
||||
|
||||
8) TOUR Mode (continuous camera ride)
|
||||
-------------------------------------
|
||||
Why Tour exists:
|
||||
- Back-to-back moveto segments each ease in/out.
|
||||
That can feel like: speed up -> slow down -> speed up -> slow down.
|
||||
|
||||
Tour runs ONE continuous move:
|
||||
- One acceleration at the start,
|
||||
- Constant motion through intermediate waypoints,
|
||||
- One deceleration at the end.
|
||||
|
||||
Syntax:
|
||||
tour <total_ms> [mode]
|
||||
... waypoint commands ...
|
||||
endtour
|
||||
|
||||
Fast compact notecard syntax:
|
||||
tour <total_ms> [mode] <idx1> <idx2> ... [fovdeg <a> <b>|fov <radA> <radB>]
|
||||
|
||||
- mode defaults to "linear"
|
||||
- supported mode strings (engine):
|
||||
linear / line
|
||||
spline
|
||||
constant / noease
|
||||
ease_in, ease_out, ease_in_out
|
||||
smoothstep, cubic, quint
|
||||
You can combine spline + profile using "+", e.g. "spline+ease_in_out".
|
||||
Use "spline+noease" when a long tour must visibly start immediately.
|
||||
|
||||
|
||||
Inside a tour block:
|
||||
- moveto <idx> [ms] -> adds waypoint (ms ignored inside tour)
|
||||
- goto <idx> -> same as moveto
|
||||
- load <idx> -> adds waypoint (still part of tour; no instant cut inside)
|
||||
- wait <ms> -> HOLD at the most recent waypoint (see below)
|
||||
- fovdeg <a> <b> -> optional FOV ramp across the tour
|
||||
- fov <a> <b> -> optional FOV ramp in radians
|
||||
- waypoint lines may include w=<weight> or speed=<factor>
|
||||
|
||||
Example: 3-waypoint linear tour
|
||||
tour 9000 linear
|
||||
moveto 1
|
||||
moveto 2
|
||||
moveto 3
|
||||
endtour
|
||||
|
||||
Example: 4-waypoint spline tour
|
||||
tour 12000 spline
|
||||
moveto 10
|
||||
moveto 11
|
||||
moveto 12
|
||||
moveto 13
|
||||
endtour
|
||||
|
||||
Fast compact equivalent:
|
||||
tour 12000 spline 10 11 12 13
|
||||
|
||||
Use the compact form when the tour only needs waypoints and optional FOV ramp.
|
||||
It avoids waiting for many separate notecard line reads before the tour can
|
||||
start. Use the block form when you need waypoint holds or per-waypoint weights.
|
||||
The compact form starts directly at its first preset; put `load <same idx>`
|
||||
before it if you want the viewer camera to be placed there first.
|
||||
|
||||
Wait inside tour = HOLD
|
||||
- In a tour, "wait <ms>" inserts a hold at the most recent waypoint.
|
||||
- Holds add extra time ON TOP of the tour movement time.
|
||||
|
||||
Example with hold:
|
||||
tour 7000 linear
|
||||
moveto 1
|
||||
wait 1500
|
||||
moveto 2
|
||||
moveto 3
|
||||
endtour
|
||||
|
||||
Edge case:
|
||||
- If a tour has fewer than 2 waypoints at endtour, it is ignored and the
|
||||
playlist continues.
|
||||
|
||||
9) LOCK (focus only)
|
||||
--------------------
|
||||
Lock sets the camera focus to a fixed target while the camera position can
|
||||
still move (moveto/tour/follow can still move position).
|
||||
|
||||
Chat usage:
|
||||
/88 lock on
|
||||
Locks focus to a point in front of your current camera.
|
||||
|
||||
/88 lock on <128,128,30>
|
||||
Locks focus to a fixed world point.
|
||||
|
||||
/88 lock on 01234567-89ab-cdef-0123-456789abcdef
|
||||
Locks focus to an object/avatar UUID.
|
||||
|
||||
/88 lock off
|
||||
|
||||
Notes:
|
||||
- In local chat, vectors with spaces are accepted:
|
||||
<128, 128, 30> is OK in chat.
|
||||
- In playlists, write vectors without spaces:
|
||||
<128,128,30>
|
||||
(Vectors with spaces are not supported in playlists.)
|
||||
|
||||
Playlist note:
|
||||
- In notecards, "lock on" must be written exactly as "on" to enable.
|
||||
(Chat also accepts "1"/"true", but notecards do not.)
|
||||
|
||||
10) FOLLOW (capture follow)
|
||||
---------------------------
|
||||
Follow keeps the camera position/focus relative to a moving target. It is
|
||||
intended for tracking shots when no moveto/tour is running.
|
||||
|
||||
Chat usage:
|
||||
/88 follow on
|
||||
Follow yourself (default target), mode world, transition 0.
|
||||
|
||||
/88 follow on <uuid> world 500
|
||||
Capture-follow a target, in world mode, blending in over 500ms.
|
||||
|
||||
/88 follow on <uuid> yaw 800
|
||||
Yaw-only follow (nice for vehicles), blend over 800ms.
|
||||
|
||||
/88 follow off
|
||||
|
||||
Modes:
|
||||
- world: offsets in world space
|
||||
- local: offsets in target's rotation space
|
||||
- yaw: only yaw rotation used (stabilizes banking/tilt)
|
||||
|
||||
Chat note:
|
||||
- When you want to provide mode or transition in chat, include the target UUID.
|
||||
The short form "/88 follow on yaw 500" is not parsed as mode+transition.
|
||||
|
||||
Playlist follow (notecard):
|
||||
- Syntax:
|
||||
follow on|off [uuid] [modeInt] [transition_ms]
|
||||
|
||||
- modeInt: 0=yaw, 1=local, 2=world
|
||||
|
||||
Deprecated / not supported in playlists (current build):
|
||||
- Explicit follow offsets in notecards:
|
||||
follow on <uuid> <posOffset> <focusOffset> yaw|local|world <transition_ms>
|
||||
(This syntax is not parsed by the playlist helper; it will behave as capture-follow instead.)
|
||||
|
||||
11) Markers ("show cams") - helper script
|
||||
-----------------------------------------
|
||||
Requires HS_CamMarkers.lsl in the HUD.
|
||||
|
||||
/88 show cams [N]
|
||||
Rezzes up to N clickable markers near you, one per preset found.
|
||||
|
||||
/88 hide cams
|
||||
Cleans up markers.
|
||||
|
||||
Clicking a marker:
|
||||
- Loads that preset (moveto by default)
|
||||
- Interrupts any running playlist
|
||||
|
||||
12) Engine Config (HS_CamEngine.properties)
|
||||
-------------------------------------------
|
||||
Engine reads tuning values from a notecard named:
|
||||
|
||||
HS_CamEngine.properties
|
||||
|
||||
Commands:
|
||||
/88 cfg reload
|
||||
/88 cfg dump
|
||||
|
||||
Common keys:
|
||||
move_step=0.025
|
||||
follow_step=0.05
|
||||
default_move_ms=3000
|
||||
default_focus_dist=10.0
|
||||
|
||||
move_pos_lag=0.5
|
||||
move_focus_lag=0.5
|
||||
follow_pos_lag=0.5
|
||||
follow_focus_lag=0.5
|
||||
|
||||
pos_threshold=0.02
|
||||
focus_threshold=0.02
|
||||
|
||||
tour_cam_min_interval=0.033
|
||||
tour_pos_epsilon=0.005
|
||||
tour_focus_epsilon=0.005
|
||||
|
||||
follow_predict=0.10
|
||||
tour_max_points=20
|
||||
|
||||
Tuning tips:
|
||||
- If motion looks steppy: lower move_step (0.03 -> 0.025).
|
||||
- If script load matters more than maximum smoothness: raise move_step slightly
|
||||
(0.025 -> 0.033).
|
||||
- Tour playback also has an internal camera-frame cap:
|
||||
tour_cam_min_interval=0.033 is about 30 Hz. Lowering move_step below this
|
||||
still computes the tour more often, but it will not send camera frames faster
|
||||
unless tour_cam_min_interval is also lowered.
|
||||
- tour_pos_epsilon and tour_focus_epsilon skip tiny duplicate tour camera
|
||||
updates. Lower values send more updates; higher values reduce script load.
|
||||
- tour_max_points is capped by the scripts for safety. Very large tours also
|
||||
hit link-message payload limits, so split extremely long rides into smaller
|
||||
tours if needed.
|
||||
- If camera feels too mushy: lower move_pos_lag / move_focus_lag.
|
||||
- If follow jitters: raise thresholds slightly (0.02 -> 0.03).
|
||||
- If follow trails: raise follow_predict slightly (0.10 -> 0.12).
|
||||
|
||||
Reload behavior:
|
||||
- Reload is asynchronous.
|
||||
- During an active move/tour, base params may apply after the move ends.
|
||||
|
||||
13) Troubleshooting
|
||||
-------------------
|
||||
"Nothing happens when I type /88 ..."
|
||||
- Ensure the HUD is worn and scripts are running.
|
||||
- Use LOCAL chat, not IM.
|
||||
- Verify channel: /88 help
|
||||
|
||||
"Camera permission denied"
|
||||
- Run /88 cam on again and accept the popup.
|
||||
- Some viewers block permission popups; check viewer settings.
|
||||
|
||||
"Show cams does not rez markers"
|
||||
- Parcel may block rez (no-rez).
|
||||
- Parcel object limits may be reached.
|
||||
- Ensure HS_CamMarkers helper is present.
|
||||
|
||||
"Presets don't save"
|
||||
- idx must be > 0.
|
||||
- Linkset Data has capacity limits; avoid excessive preset counts.
|
||||
|
||||
"Movement looks choppy"
|
||||
- Region performance matters.
|
||||
- Try lowering move_step slightly.
|
||||
- Use reasonable durations (1500-5000ms for cinematic).
|
||||
|
||||
14) Best Practices & Creative Workflows
|
||||
---------------------------------------
|
||||
Preset organization:
|
||||
- 1-20: a single scene
|
||||
- 100-120: another scene
|
||||
- 900+: experiments / temporary
|
||||
|
||||
Use Tour for ride shots:
|
||||
tour 14000 spline
|
||||
moveto 1
|
||||
moveto 2
|
||||
moveto 3
|
||||
wait 1200
|
||||
moveto 4
|
||||
endtour
|
||||
|
||||
Use early cut intentionally:
|
||||
moveto 10 6000
|
||||
wait 900
|
||||
load 11
|
||||
|
||||
Combine follow + lock for action:
|
||||
follow on <subject_uuid> yaw 400
|
||||
lock on <target_uuid>
|
||||
261
HS_DollyCam_Manual_FOV_Extension
Normal file
261
HS_DollyCam_Manual_FOV_Extension
Normal file
@ -0,0 +1,261 @@
|
||||
0) IMPORTANT: RLVa REQUIRED
|
||||
|
||||
All FOV features in HS DollyCam require a viewer with RLVa support enabled.
|
||||
Firestorm: Preferences → Firestorm → RLVa → “Enable RLVa”
|
||||
If RLVa is OFF, the camera can still move, but FOV commands will do nothing.
|
||||
|
||||
HS DollyCam sets FOV via RLVa command:
|
||||
@setcam_fov:<radians>=force
|
||||
|
||||
Notes:
|
||||
- This is viewer-side. The HUD sends commands via owner chat.
|
||||
- Some viewers show “executes: @setcam_fov:...” in chat even when scripts send “quiet”.
|
||||
That display is a viewer setting and not HUD spam.
|
||||
|
||||
|
||||
1) UNITS: Radians vs Degrees
|
||||
|
||||
FOV in HS DollyCam is stored internally in RADIANS.
|
||||
|
||||
Useful references:
|
||||
- 60° = 1.04719755 rad
|
||||
- 45° = 0.78539816 rad
|
||||
- 30° = 0.52359878 rad
|
||||
- 90° = 1.57079633 rad
|
||||
|
||||
Clamp range (safety):
|
||||
- Minimum: 10°
|
||||
- Maximum: 179°
|
||||
|
||||
|
||||
2) STANDALONE FOV COMMANDS (Chat /88)
|
||||
|
||||
A) Set FOV in radians
|
||||
/88 fov <rad>
|
||||
|
||||
Examples:
|
||||
/88 fov 1.0472 (≈ 60°)
|
||||
/88 fov 0.5236 (≈ 30°)
|
||||
|
||||
B) Set FOV in degrees
|
||||
/88 fovdeg <deg>
|
||||
|
||||
Examples:
|
||||
/88 fovdeg 60
|
||||
/88 fovdeg 35
|
||||
|
||||
C) Convenience heuristic:
|
||||
If you use /88 fov with a value > 3.2 it is assumed to be degrees.
|
||||
Example:
|
||||
/88 fov 60 (interpreted as 60°, not 60 rad)
|
||||
|
||||
|
||||
3) FOV IN PLAYLIST NOTECARDS
|
||||
|
||||
You can set FOV from a playlist notecard as standalone lines.
|
||||
|
||||
Syntax:
|
||||
fov <rad> [quiet]
|
||||
fovdeg <deg> [quiet]
|
||||
|
||||
quiet:
|
||||
- 1 = quiet (default)
|
||||
- 0 = show debug output (if enabled) / more visible logs
|
||||
|
||||
Examples:
|
||||
fovdeg 60
|
||||
fovdeg 35 1
|
||||
fov 1.0472
|
||||
fov 0.7854 1
|
||||
|
||||
Tip:
|
||||
If you run a playlist with a gap_ms, FOV lines follow the same pacing rules.
|
||||
|
||||
|
||||
4) PRESETS: SAVING / STORING FOV
|
||||
|
||||
HS DollyCam presets store camera position + focus + rotation + FOV.
|
||||
|
||||
- Presets are saved in Linkset Data slots “P<idx>”
|
||||
- v2 preset format includes FOV as the 11th field:
|
||||
field index 10 = fovRad
|
||||
|
||||
Workflow:
|
||||
1) Move camera where you want it.
|
||||
2) Set FOV you want (RLVa required), e.g.:
|
||||
/88 fovdeg 35
|
||||
3) Save the preset:
|
||||
/88 save 1
|
||||
|
||||
Important:
|
||||
- The HUD stores the “last set by HUD” FOV as a fallback.
|
||||
- Best practice: explicitly set FOV, then save, for reliable dollyzoom.
|
||||
|
||||
|
||||
5) APPLYING PRESET FOV (Load / MoveTo)
|
||||
|
||||
When you load or moveto a preset, HS DollyCam can apply the preset’s stored FOV.
|
||||
|
||||
A) Chat:
|
||||
/88 load <idx> (cut)
|
||||
/88 moveto <idx> [ms] (animated)
|
||||
|
||||
B) Menu / Markers:
|
||||
- Clicking a marker or using the menu load/moveto applies preset FOV if present.
|
||||
|
||||
C) Playlists:
|
||||
- In playlists, moveto/load lines also apply preset FOV (outside tours).
|
||||
|
||||
Notes:
|
||||
- If a preset has no stored FOV, camera movement still works; FOV stays unchanged.
|
||||
|
||||
|
||||
6) TOUR FOV: KEYFRAME RAMP DURING TOURS
|
||||
|
||||
Tours are continuous rides through multiple waypoints.
|
||||
You can optionally ramp FOV during a tour.
|
||||
|
||||
A) Tour blocks in notecards:
|
||||
tour <total_ms> [mode]
|
||||
moveto <idx1> [optional weight tokens]
|
||||
moveto <idx2> ...
|
||||
fovdeg <degA> <degB> (optional, can be inside tour block)
|
||||
wait <ms> (hold at the last waypoint)
|
||||
endtour
|
||||
|
||||
B) Fast compact tour (chat or notecard):
|
||||
/88 tour <ms> [mode] <idx1> <idx2> ... [fovdeg <a> <b>]
|
||||
/88 tour <ms> [mode] <idx1> <idx2> ... [fov <radA> <radB>]
|
||||
tour <ms> [mode] <idx1> <idx2> ... [fovdeg <a> <b>]
|
||||
tour <ms> [mode] <idx1> <idx2> ... [fov <radA> <radB>]
|
||||
|
||||
Example:
|
||||
/88 tour 8000 spline 1 2 3 4 fovdeg 35 70
|
||||
tour 8000 spline 1 2 3 4 fovdeg 35 70
|
||||
|
||||
What it does:
|
||||
- Camera moves along the tour path
|
||||
- FOV is interpolated from A → B based on path progress (weighted distance)
|
||||
- FOV ticks are throttled (about 10 Hz) + deduped
|
||||
|
||||
Tip:
|
||||
FOV ramps in tours are a creative tool (stylized “zoom breathing”), not a perfect “dollyzoom”.
|
||||
|
||||
|
||||
7) DOLLYZOOM (Classic)
|
||||
|
||||
DollyZoom is a 2-point tour that changes camera distance while counter-changing FOV.
|
||||
|
||||
Chat syntax:
|
||||
/88 dollyzoom <ms> [mode] <idxA> <idxB>
|
||||
|
||||
Chat/menu DollyZoom and one-line tours use HS_CamTourCommands.lsl.
|
||||
|
||||
Notecard syntax:
|
||||
dollyzoom <ms> [mode] <idxA> <idxB>
|
||||
|
||||
Examples:
|
||||
/88 dollyzoom 4000 linear 1 2
|
||||
/88 dollyzoom 4000 1 2
|
||||
dollyzoom 5000 spline 3 4
|
||||
|
||||
How it works:
|
||||
- Builds an internal 2-point TOUR from preset A to preset B
|
||||
- Uses FOV stored in preset A and preset B:
|
||||
FOV ramps from fovA → fovB across the move
|
||||
|
||||
Requirements:
|
||||
- BOTH presets must have FOV stored (unless using “keep”, see below)
|
||||
- Best results when both presets look at the same subject (same focus point)
|
||||
|
||||
|
||||
8) DOLLYZOOM keep (Keepframe / constant framing)
|
||||
|
||||
The current build supports “keepframe” with the “keep” option:
|
||||
|
||||
Chat:
|
||||
/88 dollyzoom <ms> [mode] <idxA> <idxB> keep
|
||||
|
||||
Notecard:
|
||||
dollyzoom <ms> [mode] <idxA> <idxB> keep
|
||||
|
||||
Goal:
|
||||
- Keep the apparent size of the subject as constant as possible while moving.
|
||||
|
||||
How it works (math):
|
||||
- Let d(t) = distance between camera position and the “motif focus”
|
||||
- Define K from the start frame:
|
||||
K = d0 * tan(FOV0/2)
|
||||
- Then compute FOV per tick:
|
||||
FOV(t) = 2 * atan( K / d(t) )
|
||||
|
||||
What is “motif focus”?
|
||||
- If LOCK is ON, motif focus comes from the LOCK target (object position + offset).
|
||||
- If LOCK is OFF, motif focus is fixed to “focA” (focus from the first dolly preset).
|
||||
- Many setups also force focB = focA automatically for keep to avoid focus drift.
|
||||
|
||||
Requirements:
|
||||
- RLVa ON (for FOV updates)
|
||||
- A meaningful distance change (camera must move closer/farther to the subject)
|
||||
- Best when the focus corresponds to the actual subject (not an arbitrary point)
|
||||
|
||||
When to use LOCK with keep:
|
||||
- Use LOCK when the subject moves (avatar walking, vehicle, animated object).
|
||||
- If the subject is static and focA is correct, you do not need LOCK.
|
||||
|
||||
Notes / Limitations (Second Life reality):
|
||||
- Viewer camera smoothing (Camera Lag/Focus Lag) can make keepframe look “off”
|
||||
because the visible camera lags behind the scripted target position.
|
||||
- FOV updates are throttled and deduped; tiny changes may not be sent every frame.
|
||||
- Expect “usable” results, not perfect cinema-grade dollyzoom.
|
||||
|
||||
Performance note:
|
||||
- Tour FOV updates are intentionally throttled and deduped.
|
||||
- Tour camera frame updates are also capped by HS_CamEngine.properties:
|
||||
tour_cam_min_interval=0.033
|
||||
tour_pos_epsilon=0.005
|
||||
tour_focus_epsilon=0.005
|
||||
- Lower move_step values make the path computation run more often, but they do
|
||||
not send camera frames faster than tour_cam_min_interval unless that value is
|
||||
lowered too. Lower values increase script load.
|
||||
|
||||
|
||||
9) QUALITY TIPS (Practical)
|
||||
|
||||
A) For a strong dollyzoom:
|
||||
- Make presets with a BIG distance difference (e.g. 3m → 15m)
|
||||
- Keep focus on the same subject point (focA == focB), or use LOCK
|
||||
- Use moderate FOV ranges (e.g. 30° → 70°)
|
||||
|
||||
B) If it looks “mushy” or delayed:
|
||||
- Reduce camera lag settings (Engine properties):
|
||||
move_pos_lag / move_focus_lag too high will cause visible trailing.
|
||||
- Consider lowering smoothing for recording, then increase for casual viewing.
|
||||
|
||||
C) If dollyzoom looks like “just moving camera”:
|
||||
- Your distance to focus might not be changing (dA ≈ dB).
|
||||
- Your focus might be drifting (focA and focB are different, or moving with camera).
|
||||
- The camera path may be sideways/orbit rather than in/out.
|
||||
|
||||
|
||||
10) TROUBLESHOOTING
|
||||
|
||||
Problem: “FOV commands do nothing”
|
||||
- RLVa is OFF or viewer doesn’t support RLVa.
|
||||
- Enable RLVa in viewer settings.
|
||||
|
||||
Problem: “Chat shows executes: @setcam_fov...”
|
||||
- That is viewer-side display.
|
||||
- HUD already uses quiet mode where possible.
|
||||
|
||||
Problem: “Dollyzoom says it needs FOV in both presets”
|
||||
- Set FOV, then save preset A and preset B again:
|
||||
/88 fovdeg 35
|
||||
/88 save 1
|
||||
/88 fovdeg 70
|
||||
/88 save 2
|
||||
|
||||
Problem: “keep doesn’t feel like dollyzoom”
|
||||
- Ensure camera distance to motif changes substantially.
|
||||
- Ensure motif focus is correct (LOCK target or correct focA).
|
||||
- Lower camera lag (smoothing) if needed.
|
||||
49
project-context.md
Normal file
49
project-context.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
project_name: 'HS_DollyCam'
|
||||
user_name: 'mita'
|
||||
date: 'Wed May 06 2026'
|
||||
sections_completed: ['technology_stack', 'implementation_rules', 'patterns']
|
||||
existing_patterns_found: 7
|
||||
---
|
||||
|
||||
# Project Context for AI Agents
|
||||
|
||||
_This file contains critical rules and patterns that AI agents must follow when implementing code in this project. Focus on unobvious details that agents might otherwise miss._
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack & Versions
|
||||
|
||||
- **Language:** LSL (Linden Scripting Language)
|
||||
- **Platform:** Second Life
|
||||
- **Core Communication:** `llMessageLinked` (Linkset Message)
|
||||
- **Configuration:** `.properties` files (e.g., `HS_CamEngine.properties`)
|
||||
- **Key Components:** Specialized LSL scripts (`Controller`, `Playlist`, `imEngineTour`, `Core`, etc.)
|
||||
|
||||
## Critical Implementation Rules
|
||||
|
||||
- **Memory Management (CRITICAL):**
|
||||
- Avoid large or mixed-type lists to prevent memory pressure.
|
||||
- Minimize the use of `llParseString2List` in hot paths (e.g., timer events or frequent `llMessageLinked` receivers).
|
||||
- Use targeted parsing (token-based or separator-based) for payloads like `CE_INT_SET_CAM`.
|
||||
- Prefer short-lived data and targeted field extraction.
|
||||
- **Communication Protocol:**
|
||||
- Adhere to the defined `CE_INT_*` and `CE_CMD_*` constants for inter-script communication.
|
||||
- Use `llRegionSayTo` for marker communication when the target key is known to reduce region-wide listener wakeups.
|
||||
- **Parsing & Data Handling:**
|
||||
- For high-frequency updates (e.g., camera frames), use optimized parsing helpers as seen in `HS_CamEngineCore.lsl`.
|
||||
- When reading presets, avoid full string splitting if only a subset of fields is required.
|
||||
- **Workflow Constraints:**
|
||||
- Keep continuous tour runtime lists in script memory (`HS_CamEngineTour.lsl`) rather than Linkset Data to ensure performance during timer ticks.
|
||||
|
||||
## Existing Patterns & Conventions
|
||||
|
||||
- **Naming Conventions:**
|
||||
- Scripts follow the `HS_Cam[Name].lsl` pattern.
|
||||
- Communication constants use the `CE_CMD_`, `CE_INT_`, `MN_CMD`, etc., prefixes.
|
||||
- **Code Organization:**
|
||||
- Modules are split by responsibility (Controller, Playlist, Engine, etc.).
|
||||
- Use specialized helper scripts for secondary workflows (e.s. `HS_CamTourCommands.lsl`) to reduce memory pressure on main scripts.
|
||||
- **Error Handling/Validation:**
|
||||
- Local checks should include brace-balance and conflict marker searches.
|
||||
- Use targeted `rg` scans to detect accidental reintroductions of inefficient `llParseString2List` calls.
|
||||
Loading…
x
Reference in New Issue
Block a user