// Copyright 2025 Fiona Sweet // Free to use for any purpose - if you modify put your name ("don't blame me!") // // RLV commander. May be worn with collar. Allows wearer to give ownership to another AV // AV can perform RLV commands. Also registers with remote web server and allows others to // manipulate AV using RLV. // fun times! // ---------- Settings ---------- string BASE_PATH = "Shapes"; // Create Folder "Shapes" (case sensitive) in #RLV folder // create two folders inside "Shapes" - "Shape Normal" and "Shape Short" // put the included "Shape Normal" and "Shape Short" in each folder (1 per folder) integer PEECHANNEL = 126; //get from peepee hud integer maxpee = 100; //timer on peepee integer ispeeing = FALSE; integer REQUIRE_NEARBY = TRUE; // require controller within ~20m when issuing commands float SCAN_RANGE = 30.0; // how far to look for candidates integer CONFIG_CHANNEL = 23; // wearer-only config channel string DESC_TAG = "CTRL="; // prefix in object description for persistence string SERVER_BASE = "https://preg.jemcity.com/rlvaware"; // change me string SHARED_SECRET = "LONGRANDOMSTRING"; // used only at register float POLL_MAX = 20.0; // polling interval 5/.25 float polliterations = 20.0; string ANIM_KNEEL = "kneel"; // name of the animation in this prim’s inventory string ANIM_MASTURBATE = "masturbate"; string gLastAnim = ""; integer gHavePerms = 0; //hitch float HITCH_SCAN_RADIUS = 20.0; // how far to look for an anchor float LEASH_LENGTH = 2.0; // max distance allowed float LEASH_SLACK = 1.0; // extra slack before yank float CHECK_EVERY = 0.25; // seconds between checks integer DRAW_PARTICLES = TRUE; // set FALSE to disable the visible leash // ---------- State ---------- key gOwner; string gAvatarName; integer gIsRegistered = FALSE; integer gConsent = FALSE; // set TRUE after server confirms consent string gToken = ""; string CONTROLLER = "00000000-0000-0000-0000-000000000000"; integer gListenLocal; integer gListenConfig; integer gListenDialog; integer gDialogChan; //hitch state key gHitchObj = NULL_KEY; vector gHitchPos = ZERO_VECTOR; integer gLeashed = FALSE; integer gHadTimer = FALSE; // track HTTP requests key gReqRegister; key gReqPoll; key gReqAck; key gReqUnsub; list gNames = []; // parallel arrays for dialog paging list gKeys = []; integer gPage = 0; integer gPageSize = 9; // up to 12 buttons; reserve 3 for More/Refresh/Cancel // ---------- Helpers ---------- wearShape(string shapeFolder) { // Attach the folder contents without removing other worn items. // For system body parts like Shape, the new one replaces the old automatically. // Requires RLVa enabled in the viewer. llOwnerSay("@attachover:" + BASE_PATH + "/" + shapeFolder + "=force"); } integer playAnim(string animName) { if (!gHavePerms) return 0; if (llGetInventoryType(animName) != INVENTORY_ANIMATION) { llOwnerSay("Animation missing from HUD inventory: " + animName); return 0; } if (gLastAnim != "" && gLastAnim != animName) { llStopAnimation(gLastAnim); } llStartAnimation(animName); gLastAnim = animName; return 1; } integer stopCurrent() { if (!gHavePerms) return 0; if (gLastAnim != "") { llStopAnimation(gLastAnim); gLastAnim = ""; return 1; } return 0; } doKneel() { playAnim(ANIM_KNEEL); } doMasturbate() { playAnim(ANIM_MASTURBATE); } stopMasturbating() { stopCurrent(); } stopKneeling() { stopCurrent(); } runAlias(string msg) { /* controller commands */ if ((msg == "command off") || (msg == "commandoff") || (msg == "restore") || (msg == "reset")) { //shut up off sendRLV("@sendim=y"); sendRLV("@recvim=y"); sendRLV("@viewnote=y"); sendRLV("@recvchat=y"); sendRLV("@sendchat=y"); sendRLV("@detach=y"); //confuse off sendRLV("@shownames=y"); sendRLV("@shownametags=y"); //maphide off sendRLV("@showloc=y"); //blind off sendRLV("@setenv_daytime:1=force"); sendRLV("@setenv_ambienti=y"); sendRLV("@setenv_scenegamma=y"); sendRLV("@setenv_sunmooncolori=y"); sendRLV("@setenv_bluedensityi=y"); sendRLV("@setenv_bluehorizoni=y"); sendRLV("@setenv_cloudcolori=y"); sendRLV("@setenv_cloudcoverage=y"); sendRLV("@camdistmax:1=y"); //cuffs off sendRLV("@detachall:cuffs=force"); stopMasturbating(); stopKneeling(); wearShape("Shape Normal"); return; } if (msg == "tall") { wearShape("Shape Normal"); } if (msg == "short") { wearShape("Shape Short"); } if (msg == "pee") { llSay(PEECHANNEL, "PEE"); ispeeing = TRUE; maxpee = 100; llSetTimerEvent(CHECK_EVERY); } if ((msg == "shutup") || (msg == "shut up")) { sendRLV("@sendim=n"); sendRLV("@detach=n"); sendRLV("@recvim=n"); sendRLV("@viewnote=n"); sendRLV("@recvchat=n"); sendRLV("@setdebug_renderresolutiondivisor:1=force"); return; } if ((msg == "shutupoff") || (msg == "shutup off") || (msg == "shut up off") || (msg == "speak")) { sendRLV("@sendim=y"); sendRLV("@recvim=y"); sendRLV("@viewnote=y"); sendRLV("@recvchat=y"); sendRLV("@sendchat=y"); sendRLV("@detach=y"); return; } if (msg == "confuse") { sendRLV("@shownames=n"); sendRLV("@shownametags=n"); return; } if ((msg == "confuseoff") || (msg == "confuse off")) { sendRLV("@shownames=y"); sendRLV("@shownametags=y"); return; } if (msg == "maphide") { sendRLV("@showloc=n"); return; } if ((msg == "maphideoff") || (msg == "maphide off")) { sendRLV("@showloc=y"); return; } if (msg == "blind") { sendRLV("@setenv_daytime:-1=force"); sendRLV("@setenv_ambienti:0=force"); sendRLV("@setenv_scenegamma:0=force"); sendRLV("@setenv_sunmooncolori:0=force"); sendRLV("@setenv_bluedensityi:0=force"); sendRLV("@setenv_bluehorizoni:0=force"); sendRLV("@setenv_cloudcolori:0=force"); sendRLV("@setenv_cloudcoverage:0=force"); sendRLV("@camdistmax:1=n"); return; } if ((msg == "blindoff") || (msg == "blind off")) { sendRLV("@setenv_daytime:1=force"); sendRLV("@setenv_ambienti=y"); sendRLV("@setenv_scenegamma=y"); sendRLV("@setenv_sunmooncolor=y"); sendRLV("@setenv_bluedensityi=y"); sendRLV("@setenv_bluehorizoni=y"); sendRLV("@setenv_cloudcolori=y"); sendRLV("@setenv_cloudcoverage=y"); sendRLV("@camdistmax:1=y"); return; } if (msg == "cuffs") { sendRLV("@attachall:cuffs=force"); return; } if ((msg == "cuffs off") || (msg == "cuffsoff")) { sendRLV("@detachall:cuffs=force"); return; } if (msg == "hitch") { startHitch(); return; } if ((msg == "unhitch")||(msg == "hitchoff")||(msg == "hitch off")) { stopHitch(); return; } if (msg == "strangle") { key owner = llGetOwner(); string name = llKey2Name(owner); llSay(0,"*"+name+" chokes and gasps, struggling to breath*"); osCauseDamage(llGetOwner(), 20.0); } if (msg == "masturbate") { key owner = llGetOwner(); string name = llKey2Name(owner); llSay(0,"*"+name+" touches herself and rubs one out*"); doMasturbate(); } if (msg == "kneel") { key owner = llGetOwner(); string name = llKey2Name(owner); llSay(0,"*"+name+" obeys and kneels to Mistress*"); doKneel(); } if (msg == "stand") { stopMasturbating(); stopKneeling(); } // inside listen(channel,name,id,msg) when channel==0 and controller checks passed: string low = llToLower(msg); if (llGetSubString(low, 0, 2) == "go ") { handleGo(llGetSubString(msg, 3, -1)); // pass everything after "go " return; } } string nowUnixStr() { return (string)llGetUnixTime(); } // ---- SHA-256 helpers ---- string sha256(string s) { return llSHA256String(s); } string signRegister(string uuid, string ts) { return sha256(uuid + "|" + ts + "|" + SHARED_SECRET); } string signPoll(string uuid, string ts, string token) { return sha256(uuid + "|" + ts + "|" + token); } string signAck(string uuid, string ids, string ts, string token) { return sha256(uuid + "|" + ids + "|" + ts + "|" + token); } string signUnsub(string uuid, string ts, string token) { return sha256(uuid + "|" + ts + "|" + token); } // Execute one command from server executeCommand(string id, string type, string payload) { //type is rlv or alias //payload is command //we don't want arbitrary rlv commands at this time just aliases if (type=="alias") { runAlias(payload); } } // ---- HTTP calls ---- registerSelf() { string ts = (string)llGetUnixTime(); string body = llList2Json(JSON_OBJECT, [ "uuid", (string)gOwner, "name", gAvatarName, "ts", ts, "sig", signRegister((string)gOwner, ts) ]); // POST JSON gReqRegister = llHTTPRequest( SERVER_BASE + "/register.php", [ HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json" ], // <-- not "Content-Type" body ); } pollServer() { // if (!gIsRegistered || gToken == "") return; string ts = (string)llGetUnixTime(); string url = SERVER_BASE + "/poll.php?uuid=" + (string)gOwner + "&ts=" + ts + "&sig=" + signPoll((string)gOwner, ts, gToken); // GET (no body) gReqPoll = llHTTPRequest( url, [ HTTP_METHOD, "GET" ], "" // body must be a string; use "" ); } ackCommands(string csvIds) { if (csvIds == "" || gToken == "" || !gConsent) return; string ts = (string)llGetUnixTime(); string body = llList2Json(JSON_OBJECT, [ "uuid", (string)gOwner, "ids", csvIds, "ts", ts, "sig", signAck((string)gOwner, csvIds, ts, gToken) ]); // POST JSON (ack) gReqAck = llHTTPRequest( SERVER_BASE + "/ack.php", [ HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json" ], body ); } // wearer-triggered revoke unsubscribeRemote() { if (gToken == "") { llOwnerSay("[Remote] Not registered."); return; } string ts = (string)llGetUnixTime(); string body = llList2Json(JSON_OBJECT, [ "uuid", (string)gOwner, "ts", ts, "sig", signUnsub((string)gOwner, ts, gToken) ]); // POST JSON (unsubscribe) gReqUnsub = llHTTPRequest( SERVER_BASE + "/unsubscribe.php", [ HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json" ], body ); } integer startsWith(string s, string prefix) { return llSubStringIndex(llToLower(s), llToLower(prefix)) == 0; } // only replace literal spaces with %20 (won't double-encode existing %XX) string encodeSpacesOnly(string s) { return llDumpList2String(llParseStringKeepNulls(s, [" "], []), "%20"); } // Normalize a hop:// URL so the region segment has %20 instead of spaces string normalizeHop(string hop) { // hop format: hop://host:port/Region Name[/x/y/z] string rest = llGetSubString(hop, 6, -1); // strip "hop://" list segs = llParseStringKeepNulls(rest, ["/"], []); integer n = llGetListLength(segs); if (n >= 2) { string hostport = llList2String(segs, 0); string region = llList2String(segs, 1); integer x = (n > 2) ? (integer)llList2String(segs, 2) : 128; integer y = (n > 3) ? (integer)llList2String(segs, 3) : 128; integer z = (n > 4) ? (integer)llList2String(segs, 4) : 25; // only encode spaces in region segment region = encodeSpacesOnly(region); return "hop://" + hostport + "/" + region + "/" + (string)x + "/" + (string)y + "/" + (string)z; } // If it's malformed, just return what we got return hop; } string urlEncodeSpaces(string s) { return llDumpList2String(llParseStringKeepNulls(s, [" "], []), "%20"); } integer hasSlashCoords(string s) { return (llSubStringIndex(s, "/") != -1); } list parseSlashCoords(string s) { // returns [region, x, y, z] (z defaults 25 if missing) list parts = llParseStringKeepNulls(s, ["/"], []); integer n = llGetListLength(parts); string region = llList2String(parts, 0); integer x = (n > 1) ? (integer)llList2String(parts, 1) : 128; integer y = (n > 2) ? (integer)llList2String(parts, 2) : 128; integer z = (n > 3) ? (integer)llList2String(parts, 3) : 25; return [region, x, y, z]; } string trimAll(string s) { return llStringTrim(s, STRING_TRIM); } // Main handler: controller typed "go ..." handleGo(string arg) { arg = llStringTrim(arg, STRING_TRIM); if (arg == "") return; // CASE A: full hop URL already if (startsWith(arg, "hop://")) { string hop = normalizeHop(arg); // Try direct RLVa teleport to hop:// llOwnerSay("@tpto:" + hop + "=force"); // Clickable backup (in case viewer doesn't accept hop in @tpto) llOwnerSay("HG link (backup): " + hop); return; } // CASE B: hypergrid in host:port:Region Name[/x/y/z] integer c1 = llSubStringIndex(arg, ":"); integer c2 = (c1 != -1) ? llSubStringIndex(llGetSubString(arg, c1 + 1, -1), ":") : -1; if (c1 != -1 && c2 != -1) { string host = llGetSubString(arg, 0, c1 - 1); string after1 = llGetSubString(arg, c1 + 1, -1); integer ppos = llSubStringIndex(after1, ":"); string port = llGetSubString(after1, 0, ppos - 1); string rest = llGetSubString(after1, ppos + 1, -1); // "Region[/x/y/z]" list parts = llParseStringKeepNulls(rest, ["/"], []); string region = llList2String(parts, 0); integer x = (llGetListLength(parts) > 1) ? (integer)llList2String(parts, 1) : 128; integer y = (llGetListLength(parts) > 2) ? (integer)llList2String(parts, 2) : 128; integer z = (llGetListLength(parts) > 3) ? (integer)llList2String(parts, 3) : 25; string hop = "hop://" + host + ":" + port + "/" + encodeSpacesOnly(region) + "/" + (string)x + "/" + (string)y + "/" + (string)z; llOwnerSay("@tpto:" + hop + "=force"); llOwnerSay("HG link (backup): " + hop); return; } // CASE C: local region "Region Name[/x/y/z]" list l = llParseStringKeepNulls(arg, ["/"], []); string region = llList2String(l, 0); integer x = (llGetListLength(l) > 1) ? (integer)llList2String(l, 1) : 128; integer y = (llGetListLength(l) > 2) ? (integer)llList2String(l, 2) : 128; integer z = (llGetListLength(l) > 3) ? (integer)llList2String(l, 3) : 25; llOwnerSay("@tpto:" + region + "/" + (string)x + "/" + (string)y + "/" + (string)z + "=force"); } integer isUUID(string s) { return (llStringLength(s) >= 36); } integer isController(key speaker) { return ((string)speaker == CONTROLLER); } integer controllerInRange(key speaker) { if (!REQUIRE_NEARBY) return TRUE; list d = llGetObjectDetails(speaker, [OBJECT_POS]); if (llGetListLength(d) == 0) return TRUE; // fail-open if unavailable vector pos = llList2Vector(d, 0); vector mine = llGetRootPosition(); return (llVecDist(pos, mine) <= 20.0); } string trim(string s) { while (llStringLength(s) > 0 && llGetSubString(s, 0, 0) == " ") s = llGetSubString(s, 1, -1); while (llStringLength(s) > 0 && llGetSubString(s, -1, -1) == " ") s = llGetSubString(s, 0, -2); return s; } sendRLV(string msg) { string m = trim(msg); // strip optional leading "rlv" if (llToLower(llGetSubString(m, 0, 2)) == "rlv") { list parts = llParseStringKeepNulls(m, [" "], []); if (llGetListLength(parts) >= 2) m = llDumpList2String(llList2List(parts, 1, -1), " "); else m = ""; m = trim(m); } if (llGetSubString(m, 0, 0) != "@") return; // only @ commands llOwnerSay(m); } saveController(string uuid) { CONTROLLER = uuid; string desc = llGetObjectDesc(); integer idx = llSubStringIndex(desc, DESC_TAG); if (idx != -1) { integer end = idx + llStringLength(DESC_TAG) + 36; if (end > llStringLength(desc)) end = llStringLength(desc); desc = llGetSubString(desc, 0, idx - 1) + DESC_TAG + uuid + llGetSubString(desc, end, -1); } else { if (llStringLength(desc) > 0) desc += " "; desc += DESC_TAG + uuid; } llSetObjectDesc(desc); llOwnerSay("[RLV Relay] Controller set to " + uuid + " and saved."); } loadControllerFromDesc() { string desc = llGetObjectDesc(); integer idx = llSubStringIndex(desc, DESC_TAG); if (idx != -1) { string maybe = llGetSubString(desc, idx + llStringLength(DESC_TAG), idx + llStringLength(DESC_TAG) + 35); if (isUUID(maybe)) CONTROLLER = maybe; } } startListening() { if (gListenLocal) llListenRemove(gListenLocal); if (gListenConfig) llListenRemove(gListenConfig); if (gListenDialog) llListenRemove(gListenDialog); gListenLocal = llListen(0, "", NULL_KEY, ""); gListenConfig = llListen(CONFIG_CHANNEL, "", llGetOwner(), ""); gDialogChan = (integer)(-1 - llRound(llFrand(1000000000.0))); gListenDialog = llListen(gDialogChan, "", llGetOwner(), ""); } showStatus() { llOwnerSay("[RLV Relay] Status — Controller: " + CONTROLLER + " | Nearby required: " + (REQUIRE_NEARBY ? "yes" : "no") + " | Menu: touch or /" + (string)CONFIG_CHANNEL + " menu"); } // ---- Listing / Dialog (llGetAgentList, filtered by distance) ---- buildNearbyList() { gNames = []; gKeys = []; gPage = 0; list agents = llGetAgentList(AGENT_LIST_REGION, []); // region-wide key owner = llGetOwner(); integer len = llGetListLength(agents); integer i = 0; vector mine = llGetRootPosition(); while (i < len) { key k = llList2Key(agents, i); if (k != owner) { list d = llGetObjectDetails(k, [OBJECT_POS]); if (llGetListLength(d) >= 1) { vector pos = llList2Vector(d, 0); if (llVecDist(pos, mine) <= SCAN_RANGE) { // unique by key if (llListFindList(gKeys, [k]) == -1) { string nm = llKey2Name(k); if (nm == "") nm = "(unknown)"; string shortKey = llGetSubString((string)k, -4, -1); gKeys += [k]; gNames += [nm + "…" + shortKey]; } } } } i = i + 1; } } showPage() { integer total = llGetListLength(gNames); integer pages = (total + gPageSize - 1) / gPageSize; if (pages < 1) pages = 1; if (gPage >= pages) gPage = pages - 1; integer start = gPage * gPageSize; integer end = start + gPageSize - 1; if (end >= total) end = total - 1; list buttons = []; integer i = start; while (i <= end && i >= 0) { buttons += [llList2String(gNames, i)]; i = i + 1; } if (pages > 1) buttons += ["More"]; buttons += ["Refresh", "Cancel"]; string prompt = "Pick a controller (" + (string)total + " nearby)\n"; prompt += "Page " + (string)(gPage + 1) + "/" + (string)pages + "\n"; prompt += "Tap a name to set as controller."; llDialog(llGetOwner(), prompt, buttons, gDialogChan); } openMenu() { buildNearbyList(); showPage(); } //hitch helpers // --- particles (visual leash) --- leashParticlesOn(key target) { if (!DRAW_PARTICLES) return; list ps = [ PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_DROP, PSYS_PART_START_COLOR, <1,1,1>, PSYS_PART_END_COLOR, <1,1,1>, PSYS_PART_START_ALPHA, 1.0, PSYS_PART_END_ALPHA, 1.0, PSYS_PART_START_SCALE, <0.02,0.02,0.0>, PSYS_PART_END_SCALE, <0.02,0.02,0.0>, PSYS_SRC_MAX_AGE, 0.0, PSYS_PART_MAX_AGE, 0.2, PSYS_SRC_BURST_RATE, 0.01, PSYS_SRC_BURST_PART_COUNT, 1, PSYS_SRC_TARGET_KEY, target, PSYS_SRC_BURST_SPEED_MIN, 0.0, PSYS_SRC_BURST_SPEED_MAX, 0.0, PSYS_PART_FLAGS, PSYS_PART_TARGET_POS_MASK|PSYS_PART_INTERP_COLOR_MASK|PSYS_PART_INTERP_SCALE_MASK ]; llParticleSystem(ps); } leashParticlesOff(){ llParticleSystem([]); } // --- small helpers --- vector clampToRadius(vector from, vector to, float r) { vector d = to - from; float dist = llVecMag(d); if (dist <= r) return to; return from + (d / dist) * r; } // multi-direction sweep to find nearest object via llCastRay key nearestObjectKey = NULL_KEY; float nearestObjectDist = 999999.0; vector nearestObjectPos = ZERO_VECTOR; // Return the first hit object key along a ray, or NULL_KEY key castForObject(vector from, vector to) { // Only objects (reject avatars), 1 hit max, fastest path list hits = llCastRay( from, to, [ RC_REJECT_TYPES, RC_REJECT_AGENTS, RC_MAX_HITS, 1 ] ); if (llGetListLength(hits) >= 1) return (key)llList2String(hits, 0); return NULL_KEY; } // Get object world position safely vector getObjPos(key obj) { list d = llGetObjectDetails(obj, [OBJECT_POS]); if (llGetListLength(d) > 0) return llList2Vector(d, 0); return ZERO_VECTOR; } key findNearestObject(vector origin, float radius) { nearestObjectKey = NULL_KEY; nearestObjectDist = 999999.0; nearestObjectPos = ZERO_VECTOR; integer steps = 16; // 16 spokes around you integer i = 0; while (i < steps) { float ang = (TWO_PI / steps) * i; vector dir = ; key k = castForObject(origin, origin + dir * radius); if (k && k != llGetKey()) // ignore self { vector p = getObjPos(k); if (p != ZERO_VECTOR) { float d = llVecDist(origin, p); if (d < nearestObjectDist) { nearestObjectDist = d; nearestObjectKey = k; nearestObjectPos = p; } } } i += 1; } // one straight-down ray (often hits floor/ground/fixture) key kd = castForObject(origin, origin + <0,0,-radius>); if (kd && kd != llGetKey()) { vector pd = getObjPos(kd); if (pd != ZERO_VECTOR) { float dd = llVecDist(origin, pd); if (dd < nearestObjectDist) { nearestObjectDist = dd; nearestObjectKey = kd; nearestObjectPos = pd; } } } return nearestObjectKey; } // --- start/stop leash --- startHitch() { vector me = llGetRootPosition(); key k = findNearestObject(me, HITCH_SCAN_RADIUS); if (!k) { llOwnerSay("[Hitch] No object found within " + (string)HITCH_SCAN_RADIUS + " m."); return; } gHitchObj = k; gHitchPos = nearestObjectPos; // snapshot; object might move, you can refresh if desired gLeashed = TRUE; leashParticlesOn(gHitchObj); if (!gHadTimer) { gHadTimer = TRUE; llSetTimerEvent(CHECK_EVERY); } llOwnerSay("[Hitch] Leashed to object " + (string)gHitchObj + " at " + (string)gHitchPos + "."); } stopHitch() { gLeashed = FALSE; gHitchObj = NULL_KEY; gHitchPos = ZERO_VECTOR; leashParticlesOff(); // keep timer if you already use it for other features if (gHadTimer) { gHadTimer = FALSE; } llOwnerSay("[Hitch] Released."); } // --- rule: keep within leash length, yank back with RLVa tpto (grid-local) --- leashEnforce() { if (!gLeashed || !gHitchObj) return; // if target moved (optional): refresh pos list d = llGetObjectDetails(gHitchObj, [OBJECT_POS]); if (llGetListLength(d) > 0) gHitchPos = llList2Vector(d, 0); vector me = llGetRootPosition(); float dist = llVecDist(me, gHitchPos); if (dist > (LEASH_LENGTH + LEASH_SLACK)) { // yank to the boundary point vector target = clampToRadius(gHitchPos, me, LEASH_LENGTH); // convert to region coords string string region = llGetRegionName(); string tp = region + "/" + (string)((integer)target.x) + "/" + (string)((integer)target.y) + "/" + (string)((integer)target.z); // RLVa remote TP (within current grid) llOwnerSay("@tpto:" + tp + "=force"); // viewer handles region->global conversion // (RLVa @tpto syntax documented here) // ref: Catznip RLVa list } } // ---------- Events ---------- default { state_entry() { llRequestPermissions(llGetOwner(), PERMISSION_TRIGGER_ANIMATION); loadControllerFromDesc(); startListening(); gOwner = llGetOwner(); gAvatarName = llKey2Name(gOwner); gIsRegistered = FALSE; gToken = ""; gConsent = FALSE; registerSelf(); llSetTimerEvent(CHECK_EVERY); llOwnerSay("[Remote] Type '/"+(string)CONFIG_CHANNEL+" unsubscribe' to revoke internet control."); llOwnerSay("[RLV Relay] Ready. Controller: " + CONTROLLER + " | Nearby required: " + (REQUIRE_NEARBY ? "yes" : "no") + " | Config /" + (string)CONFIG_CHANNEL + " | Touch for menu"); } attach(key id) { if (id) { loadControllerFromDesc(); startListening(); llOwnerSay("[RLV Relay] Attached."); llRequestPermissions(id, PERMISSION_TRIGGER_ANIMATION); gOwner = id; gAvatarName = llKey2Name(gOwner); gIsRegistered = FALSE; gToken = ""; gConsent = FALSE; registerSelf(); llSetTimerEvent(CHECK_EVERY); } else { if (gListenLocal) llListenRemove(gListenLocal); if (gListenConfig) llListenRemove(gListenConfig); if (gListenDialog) llListenRemove(gListenDialog); llSetTimerEvent(0.0); } } on_rez(integer sp) { loadControllerFromDesc(); startListening(); gOwner = llGetOwner(); gAvatarName = llKey2Name(gOwner); gIsRegistered = FALSE; gToken = ""; gConsent = FALSE; registerSelf(); llSetTimerEvent(CHECK_EVERY); } run_time_permissions(integer perm) { gHavePerms = (perm & PERMISSION_TRIGGER_ANIMATION); if (!gHavePerms) { llOwnerSay("I need animation permission. Touch again after granting."); } } touch_start(integer n) { if (llDetectedKey(0) == llGetOwner()) openMenu(); } timer() { if (ispeeing) { maxpee--; if (maxpee<1) { llSay(PEECHANNEL, "STOP"); ispeeing = FALSE; } } else { polliterations--; if (polliterations<1) { polliterations = POLL_MAX; if (gIsRegistered && gToken != "") { pollServer(); // will no-op or handle errors as you already do } } } leashEnforce(); } listen(integer channel, string name, key id, string msg) { if (channel == 0) { if (!isController(id)) return; if (!controllerInRange(id)) return; runAlias(msg); return; } if (channel == CONFIG_CHANNEL && id == llGetOwner()) { list parts = llParseString2List(llToLower(msg), [" "], []); integer cnt = llGetListLength(parts); if (cnt > 0) { string cmd = llList2String(parts, 0); if (cmd == "unsubscribe") { unsubscribeRemote(); // stops on server; client stops polling below on response return; } if (cmd == "menu" || cmd == "scan") { openMenu(); return; } if (cmd == "nearby" && cnt >= 2) { string v = llList2String(parts, 1); if (v == "on" || v == "yes") REQUIRE_NEARBY = TRUE; else if (v == "off" || v == "no") REQUIRE_NEARBY = FALSE; llOwnerSay("[RLV Relay] Nearby requirement: " + (REQUIRE_NEARBY ? "on" : "off")); return; } if (cmd == "controller" && cnt >= 2) { string newCtrl = llList2String(llParseString2List(msg, [" "], []), 1); if (isUUID(newCtrl)) saveController(newCtrl); else llOwnerSay("[RLV Relay] That doesn't look like a UUID."); return; } if (cmd == "status") { showStatus(); return; } } llOwnerSay("[RLV Relay] Commands: /" + (string)CONFIG_CHANNEL + " menu|scan | nearby on|off | controller | status"); return; } if (channel == gDialogChan && id == llGetOwner()) { if (msg == "Cancel") return; if (msg == "Refresh") { openMenu(); return; } if (msg == "More") { gPage = gPage + 1; showPage(); return; } integer idx = llListFindList(gNames, [msg]); if (idx != -1) { key chosen = (key)llList2String(gKeys, idx); if (chosen) { saveController((string)chosen); llInstantMessage(chosen, "/me set you as controller for their RLV relay."); showStatus(); } } else { openMenu(); // list changed between scan & click } } } http_response(key req, integer status, list meta, string body) { if (req == gReqRegister) { if (status == 200) { string tok = llJsonGetValue(body, ["token"]); string consent_url = llJsonGetValue(body, ["consent_url"]); integer consent = (integer)llJsonGetValue(body, ["consent"]); if (tok != JSON_INVALID && tok != "") { gToken = tok; gIsRegistered = TRUE; gConsent = consent; if (!gConsent && consent_url != JSON_INVALID && consent_url != "") { llOwnerSay("[Remote] Please approve remote control in your browser."); llLoadURL(llGetOwner(), "Approve remote control", consent_url); } else if (gConsent) { llOwnerSay("[Remote] Consent already granted. Remote control active."); } } else { llOwnerSay("[Remote] Register failed: " + body); } } else { llOwnerSay("[Remote] Register HTTP " + (string)status); } return; } if (req == gReqPoll) { if (status == 200) { // consent is ON (or the server doesn’t require it) if (!gConsent) llOwnerSay("[Remote] Consent confirmed. Remote control active."); gConsent = TRUE; if (body != "[]") { list ids = []; integer i = 0; while (TRUE) { string cid = llJsonGetValue(body,[i,"id"]); if (cid == JSON_INVALID) { jump done; } string ctype = llJsonGetValue(body,[i,"type"]); string pay = llJsonGetValue(body,[i,"payload"]); executeCommand(cid, ctype, pay); ids += [cid]; i++; } @done; if (llGetListLength(ids) > 0) ackCommands(llDumpList2String(ids, ",")); } return; } if (status == 403) { // consent OFF (not granted or revoked) if (gConsent) llOwnerSay("[Remote] Consent revoked/not granted. Polling will keep checking."); gConsent = FALSE; return; } // other errors: keep trying later return; } if (req == gReqAck) { return; } if (req == gReqUnsub) { if (status == 200) { gConsent = FALSE; llOwnerSay("[Remote] You are unsubscribed. Remote control is OFF."); } else { llOwnerSay("[Remote] Unsubscribe failed (HTTP " + (string)status + ")."); } return; } } }