TIDE-SL-LSL-Mindweave
π(55) | Allπ 2026-09-23 00:36:45 -0700
β²οΈπ2026-09-23 00:39:11 -0700 | βοΈinfinivaeria | π·οΈ[mindweave]
(πͺ)
// RustForth
// LSL client for Tide-style REST API endpoints.
// =========================
// ESCAPE / UNESCAPE
// (kept exactly as provided by you)
// =========================
string ESCAPE_CHARACTER = "\\";
string ESCAPE_CHARACTER_REPLACE = "\\\\";
string escape(string avatar_message)
{
list avatar_message_string = llParseString2List(avatar_message, [""], []);
integer counter = 0;
list string_chars = [];
for (counter = 0; counter < llStringLength(avatar_message); counter++)
{
string_chars = string_chars + llGetSubString(avatar_message, counter, counter);
if (llGetSubString(avatar_message, counter, counter) == ESCAPE_CHARACTER)
{
string_chars = llListReplaceList(string_chars, ["/"], counter, counter );
}
else if (llGetSubString(avatar_message, counter, counter) == ":")
{
string_chars = llListReplaceList(string_chars, ["\\:"], counter, counter );
}
else if (llGetSubString(avatar_message, counter, counter) == "[")
{
string_chars = llListReplaceList(string_chars, ["\\["], counter, counter );
}
else if (llGetSubString(avatar_message, counter, counter) == "]")
{
string_chars = llListReplaceList(string_chars, ["\\]"], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "{")
{
string_chars = llListReplaceList(string_chars, ["\\{"], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "}")
{
string_chars = llListReplaceList(string_chars, ["\\}"], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "'")
{
string_chars = llListReplaceList(string_chars, ["\\'"], counter, counter );
}
else if (llGetSubString(avatar_message, counter, counter) == "\"")
{
string_chars = llListReplaceList(string_chars, ["\\\""], counter, counter );
}
else if (llGetSubString(avatar_message, counter, counter) == ".")
{
string_chars = llListReplaceList(string_chars, ["\\."], counter, counter );
}
}
return llDumpList2String(string_chars, "");
}
string unescape(string avatar_message)
{
list avatar_message_string = llParseString2List(avatar_message, [""], []);
integer counter = 0;
list string_chars = [];
for (counter = 0; counter < llStringLength(avatar_message); counter++)
{
string_chars = string_chars + llGetSubString(avatar_message, counter, counter);
if (llGetSubString(avatar_message, counter, counter) == "\\/")
{
string_chars = llListReplaceList(string_chars, [ESCAPE_CHARACTER], counter, counter );
//llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "\\:")
{
string_chars = llListReplaceList(string_chars, [":"], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "\\[")
{
string_chars = llListReplaceList(string_chars, ["["], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "\\]")
{
string_chars = llListReplaceList(string_chars, ["]"], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "\\{")
{
string_chars = llListReplaceList(string_chars, ["{"], counter, counter );
// llSay(0, (string)string_chars);
}
else if (llGetSubString(avatar_message, counter, counter) == "\\}")
{
string_chars = llListReplaceList(string_chars, ["}"], counter, counter );
// llSay(0, (string)string_chars);
}
}
return llDumpList2String(string_chars, "");
}
// =========================
// GLOBALS
// =========================
string SERVERURL = "https://stimky.info";
string API_KEY = "";
string BRIDGE_TOKEN = ""; // Set this to the MSSL_FORTH_BRIDGE_TOKEN value.
list requests = [];
list POST_HEADERS = [
HTTP_METHOD, "POST",
HTTP_MIMETYPE, "application/json"
];
list kv = []; // local cache
integer CHANNEL_OUTPUT = 0; // public channel
integer CHANNEL_INPUT = 1111; // set variables etc to a different channel
integer BRIDGE_ENABLED = FALSE;
integer BRIDGE_POLL_IN_FLIGHT = FALSE;
float BRIDGE_POLL_SECONDS = 3.0;
string SESSION_ID = "";
key notecard_query = NULL_KEY;
integer notecard_line = 0;
string notecard_name = "";
string notecard_source = "";
// =========================
// KV helpers (added β required by http_response)
// =========================
string kv_get(string keyVal)
{
integer i = llListFindList(kv, [keyVal]);
if (i != -1 && (i + 1) < llGetListLength(kv)) return llList2String(kv, i + 1);
return "";
}
kv_set(string keyVal, string value)
{
integer i = llListFindList(kv, [keyVal]);
if (i != -1)
{
kv = llListReplaceList(kv, [value], i + 1, i + 1);
}
else
{
kv += [keyVal, value];
}
}
kv_delete(string keyVal)
{
integer i = llListFindList(kv, [keyVal]);
if (i != -1) kv = llDeleteSubList(kv, i, i + 1);
}
// =========================
// OUTPUT HELPERS
// =========================
say(string msg)
{
llSay(CHANNEL_OUTPUT, msg);
}
say_chunks(integer channel, string msg)
{
integer start = 0;
integer length = llStringLength(msg);
while (start < length)
{
llSay(channel, llGetSubString(msg, start, start + 899));
start += 900;
}
}
debug(string msg)
{
llOwnerSay(msg);
}
show_help()
{
debug("RubyForth/Forth: separate sequential statements with ';'. Example: /1111 ruby count = 1; puts count; count += 1; puts count");
debug("In a RubyForth notecard, use: forth do <Forth source> end. Both languages share variables and the active session.");
debug("Session commands: /1111 session <safe-name> | /1111 session-reset. The default session is this prim UUID.");
}
forth_post(string endpoint, string action, string argument, string payload)
{
payload = llJsonSetValue(payload, ["session_id"], SESSION_ID);
key request_id = llHTTPRequest(SERVERURL + endpoint, POST_HEADERS, payload);
requests += [(string)request_id, action, argument];
}
string forth_name_payload(string name)
{
return llJsonSetValue("{}", ["name"], name);
}
string forth_file_payload(string name, string content)
{
string json = llJsonSetValue("{}", ["name"], name);
return llJsonSetValue(json, ["content"], content);
}
string forth_source_payload(string source, integer max_steps)
{
string json = llJsonSetValue("{}", ["source"], source);
if (max_steps > 0) json = llJsonSetValue(json, ["max_steps"], (string)max_steps);
return json;
}
string forth_name_steps_payload(string name, integer max_steps)
{
string json = llJsonSetValue("{}", ["name"], name);
return llJsonSetValue(json, ["max_steps"], (string)max_steps);
}
string forth_bridge_payload()
{
return llJsonSetValue("{}", ["token"], BRIDGE_TOKEN);
}
bridge_poll()
{
if (!BRIDGE_ENABLED || BRIDGE_POLL_IN_FLIGHT || llStringLength(BRIDGE_TOKEN) == 0) return;
BRIDGE_POLL_IN_FLIGHT = TRUE;
forth_post("/forth/bridge/poll", "bridge", "", forth_bridge_payload());
}
float color_component(string value)
{
float component = (float)value;
if (component > 1.0) component = component / 255.0;
if (component < 0.0) return 0.0;
if (component > 1.0) return 1.0;
return component;
}
float alpha_component(string value)
{
return color_component(value);
}
run_forth_calls(string body)
{
integer index = 0;
string call = llJsonGetValue(body, ["calls", index]);
while (call != JSON_INVALID)
{
string operation = llJsonGetValue(call, ["op"]);
if (operation == "say")
{
llSay((integer)llJsonGetValue(call, ["channel"]), llJsonGetValue(call, ["message"]));
}
else if (operation == "whisper")
{
llWhisper((integer)llJsonGetValue(call, ["channel"]), llJsonGetValue(call, ["message"]));
}
else if (operation == "shout")
{
llShout((integer)llJsonGetValue(call, ["channel"]), llJsonGetValue(call, ["message"]));
}
else if (operation == "region_say")
{
llRegionSay((integer)llJsonGetValue(call, ["channel"]), llJsonGetValue(call, ["message"]));
}
else if (operation == "owner_say")
{
llOwnerSay(llJsonGetValue(call, ["message"]));
}
else if (operation == "set_text")
{
llSetText(
llJsonGetValue(call, ["text"]),
<color_component(llJsonGetValue(call, ["red"])), color_component(llJsonGetValue(call, ["green"])), color_component(llJsonGetValue(call, ["blue"]))>,
alpha_component(llJsonGetValue(call, ["alpha"]))
);
}
else if (operation == "set_color")
{
llSetColor(
<color_component(llJsonGetValue(call, ["red"])), color_component(llJsonGetValue(call, ["green"])), color_component(llJsonGetValue(call, ["blue"]))>,
(integer)llJsonGetValue(call, ["face"])
);
}
else if (operation == "set_alpha")
{
llSetAlpha(alpha_component(llJsonGetValue(call, ["alpha"])), (integer)llJsonGetValue(call, ["face"]));
}
else if (operation == "play_sound")
{
llPlaySound(llJsonGetValue(call, ["sound"]), alpha_component(llJsonGetValue(call, ["volume"])));
}
else if (operation == "set_timer")
{
llSetTimerEvent((float)llJsonGetValue(call, ["seconds"]));
}
else if (operation == "set_region_pos")
{
llSetRegionPos(<(float)llJsonGetValue(call, ["x"]), (float)llJsonGetValue(call, ["y"]), (float)llJsonGetValue(call, ["z"])>);
}
else if (operation == "link_message")
{
llMessageLinked(
(integer)llJsonGetValue(call, ["link"]),
(integer)llJsonGetValue(call, ["code"]),
llJsonGetValue(call, ["message"]),
(key)llJsonGetValue(call, ["id"])
);
}
index++;
call = llJsonGetValue(body, ["calls", index]);
}
}
// =========================
// DEFAULT STATE
// =========================
default
{
on_rez(integer start_parameter)
{
llOwnerSay("Reloading variable functions for the tide to second life server interaction... Input on /1111 ..");
}
state_entry()
{
SESSION_ID = (string)llGetKey();
llListen(CHANNEL_INPUT, "", NULL_KEY, "");
debug("Tide stack ready. Use /1111 help for RubyForth, Forth, session, and bridge syntax.");
}
listen(integer chan, string name, key id, string msg)
{
if (id != llGetOwner()) return;
list parts = llParseString2List(msg, [" "], []);
integer n = llGetListLength(parts);
if (n == 0) return;
string cmd = llToLower(llList2String(parts, 0));
// =========================
// CHANNEL SWITCH
// =========================
if (cmd == "channel" && n >= 2)
{
CHANNEL_OUTPUT = (integer)llList2String(parts, 1);
//debug("Output channel set to " + (string)CHANNEL_OUTPUT);
return;
}
if (cmd == "help")
{
show_help();
return;
}
if (cmd == "session" && n == 2)
{
SESSION_ID = llList2String(parts, 1);
debug("Active language session: " + SESSION_ID);
return;
}
if (cmd == "session-reset")
{
SESSION_ID = (string)llGetKey();
debug("Active language session reset to prim UUID: " + SESSION_ID);
return;
}
if (cmd == "bridge-on")
{
if (llStringLength(BRIDGE_TOKEN) == 0)
{
debug("Set BRIDGE_TOKEN in this script before enabling the Second Life bridge.");
return;
}
BRIDGE_ENABLED = TRUE;
llSetTimerEvent(BRIDGE_POLL_SECONDS);
bridge_poll();
return;
}
if (cmd == "bridge-off")
{
BRIDGE_ENABLED = FALSE;
BRIDGE_POLL_IN_FLIGHT = FALSE;
llSetTimerEvent(0.0);
return;
}
if (cmd == "bridge-poll")
{
bridge_poll();
return;
}
// =========================
// SET
// =========================
if (cmd == "set" && n >= 3)
{
string keyVal = escape(llList2String(parts, 1));
string value = escape(llDumpList2String(llList2List(parts, 2, -1), " "));
string json = "{}";
json = llJsonSetValue(json, [keyVal], value);
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/set", POST_HEADERS, json);
requests += [(string)req, "set", keyVal];
//say("Sent set request for " + keyVal);
return;
}
// =========================
// GET
// =========================
if (cmd == "get" && n >= 2)
{
string keyVal = escape(llList2String(parts, 1));
string json = "{}";
json = llJsonSetValue(json, ["name"], keyVal);
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/get", POST_HEADERS, json);
requests += [(string)req, "get", keyVal];
//say("Sent get request for " + keyVal);
return;
}
// =========================
// VIEW
// =========================
if (cmd == "view")
{
string json = llJsonSetValue("{}", ["action"], "view");
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/view", POST_HEADERS, json);
requests += [(string)req, "view", ""];
//say("Sent view request");
return;
}
// =========================
// DELETE
// =========================
if (cmd == "delete" && n >= 2)
{
string keyVal = escape(llList2String(parts, 1));
string json = llJsonSetValue("{}", ["name"], keyVal);
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/delete", POST_HEADERS, json);
requests += [(string)req, "delete", keyVal];
// say("Sent delete request for " + keyVal);
return;
}
// =========================
// CLEAR
// =========================
if (cmd == "clear")
{
string json = llJsonSetValue("{}", ["action"], "clear");
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/clear", POST_HEADERS, json);
requests += [(string)req, "clear", ""];
//say("Sent clear request");
return;
}
// =========================
// STATUS
// =========================
if (cmd == "status")
{
string json = llJsonSetValue("{}", ["action"], "status");
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/status", POST_HEADERS, json);
requests += [(string)req, "status", ""];
//say("Sent status request");
return;
}
// =========================
// HISTORY
// =========================
if (cmd == "history")
{
string json = llJsonSetValue("{}", ["action"], "history");
json = llJsonSetValue(json, ["session_id"], SESSION_ID);
key req = llHTTPRequest(SERVERURL + "/vars/history", POST_HEADERS, json);
requests += [(string)req, "history", ""];
//say("Sent history request");
return;
}
// Run Forth and announce its output on the selected output channel.
if (cmd == "forth-steps" && n >= 3)
{
integer max_steps = (integer)llList2String(parts, 1);
if (max_steps < 1 || max_steps > 1000000)
{
debug("max_steps must be between 1 and 1000000.");
return;
}
string source = llDumpList2String(llList2List(parts, 2, -1), " ");
forth_post("/forth/eval", "forth", (string)CHANNEL_OUTPUT, forth_source_payload(source, max_steps));
return;
}
if (cmd == "forth" && n >= 2)
{
string source = llDumpList2String(llList2List(parts, 1, -1), " ");
string json = llJsonSetValue("{}", ["source"], source);
forth_post("/forth/eval", "forth", (string)CHANNEL_OUTPUT, json);
return;
}
if (cmd == "hybrid" && n >= 2)
{
string source = llDumpList2String(llList2List(parts, 1, -1), " ");
forth_post("/forth/eval", "forth", (string)CHANNEL_OUTPUT, forth_source_payload(source, 0));
return;
}
// Run Forth and announce only this result on an arbitrary SL channel.
if (cmd == "forth-say" && n >= 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string source = llDumpList2String(llList2List(parts, 2, -1), " ");
string json = llJsonSetValue("{}", ["source"], source);
forth_post("/forth/eval", "forth", (string)reply_channel, json);
return;
}
if (cmd == "hybrid-say" && n >= 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string source = llDumpList2String(llList2List(parts, 2, -1), " ");
forth_post("/forth/eval", "forth", (string)reply_channel, forth_source_payload(source, 0));
return;
}
// Run Ruby-shaped source through the server's RubyForth compiler.
if (cmd == "ruby-steps" && n >= 3)
{
integer max_steps = (integer)llList2String(parts, 1);
if (max_steps < 1 || max_steps > 1000000)
{
debug("max_steps must be between 1 and 1000000.");
return;
}
string source = llDumpList2String(llList2List(parts, 2, -1), " ");
forth_post("/ruby/eval", "forth", (string)CHANNEL_OUTPUT, forth_source_payload(source, max_steps));
return;
}
if (cmd == "ruby" && n >= 2)
{
string source = llDumpList2String(llList2List(parts, 1, -1), " ");
string json = llJsonSetValue("{}", ["source"], source);
forth_post("/ruby/eval", "forth", (string)CHANNEL_OUTPUT, json);
return;
}
if (cmd == "ruby-say" && n >= 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string source = llDumpList2String(llList2List(parts, 2, -1), " ");
string json = llJsonSetValue("{}", ["source"], source);
forth_post("/ruby/eval", "forth", (string)reply_channel, json);
return;
}
if (cmd == "algebra" && n >= 2)
{
string expression = llDumpList2String(llList2List(parts, 1, -1), " ");
string json = llJsonSetValue("{}", ["expression"], expression);
forth_post("/forth/algebra", "algebra", (string)CHANNEL_OUTPUT, json);
return;
}
if (cmd == "algebra-say" && n >= 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string expression = llDumpList2String(llList2List(parts, 2, -1), " ");
string json = llJsonSetValue("{}", ["expression"], expression);
forth_post("/forth/algebra", "algebra", (string)reply_channel, json);
return;
}
if (cmd == "matrix-get" && n == 2)
{
string matrix_name = llList2String(parts, 1);
forth_post("/forth/matrices/get", "matrix", (string)CHANNEL_OUTPUT, forth_name_payload(matrix_name));
return;
}
if (cmd == "matrix-get-say" && n == 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string matrix_name = llList2String(parts, 2);
forth_post("/forth/matrices/get", "matrix", (string)reply_channel, forth_name_payload(matrix_name));
return;
}
if (cmd == "file-write" && n >= 3)
{
string file_name = llList2String(parts, 1);
string content = llDumpList2String(llList2List(parts, 2, -1), " ");
forth_post("/forth/files/write", "file-write", (string)CHANNEL_OUTPUT, forth_file_payload(file_name, content));
return;
}
if (cmd == "file-read" && n == 2)
{
string file_name = llList2String(parts, 1);
forth_post("/forth/files/read", "file-read", (string)CHANNEL_OUTPUT, forth_name_payload(file_name));
return;
}
if (cmd == "file-read-say" && n == 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string file_name = llList2String(parts, 2);
forth_post("/forth/files/read", "file-read", (string)reply_channel, forth_name_payload(file_name));
return;
}
if (cmd == "files")
{
forth_post("/forth/files/list", "files", (string)CHANNEL_OUTPUT, "{}");
return;
}
if (cmd == "file-delete" && n == 2)
{
string file_name = llList2String(parts, 1);
forth_post("/forth/files/delete", "file-delete", (string)CHANNEL_OUTPUT, forth_name_payload(file_name));
return;
}
// Upload an inventory notecard as a named remote Forth program.
if (cmd == "notecard" && n == 2)
{
string card_name = llList2String(parts, 1);
if (llGetInventoryType(card_name) != INVENTORY_NOTECARD)
{
say("No notecard named " + card_name + " in this object.");
return;
}
notecard_name = card_name;
notecard_source = "";
notecard_line = 0;
notecard_query = llGetNotecardLine(notecard_name, notecard_line);
say("Loading " + notecard_name + " into the remote Forth database.");
return;
}
if (cmd == "forth-run" && n == 2)
{
string card_name = llList2String(parts, 1);
forth_post("/forth/notecards/run", "forth", (string)CHANNEL_OUTPUT, forth_name_payload(card_name));
return;
}
if (cmd == "forth-run-steps" && n == 3)
{
integer max_steps = (integer)llList2String(parts, 1);
if (max_steps < 1 || max_steps > 1000000)
{
debug("max_steps must be between 1 and 1000000.");
return;
}
string card_name = llList2String(parts, 2);
forth_post("/forth/notecards/run", "forth", (string)CHANNEL_OUTPUT, forth_name_steps_payload(card_name, max_steps));
return;
}
if (cmd == "forth-run-say" && n == 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string card_name = llList2String(parts, 2);
forth_post("/forth/notecards/run", "forth", (string)reply_channel, forth_name_payload(card_name));
return;
}
if (cmd == "ruby-run" && n == 2)
{
string card_name = llList2String(parts, 1);
forth_post("/ruby/notecards/run", "forth", (string)CHANNEL_OUTPUT, forth_name_payload(card_name));
return;
}
if (cmd == "ruby-run-steps" && n == 3)
{
integer max_steps = (integer)llList2String(parts, 1);
if (max_steps < 1 || max_steps > 1000000)
{
debug("max_steps must be between 1 and 1000000.");
return;
}
string card_name = llList2String(parts, 2);
forth_post("/ruby/notecards/run", "forth", (string)CHANNEL_OUTPUT, forth_name_steps_payload(card_name, max_steps));
return;
}
if (cmd == "ruby-run-say" && n == 3)
{
integer reply_channel = (integer)llList2String(parts, 1);
string card_name = llList2String(parts, 2);
forth_post("/ruby/notecards/run", "forth", (string)reply_channel, forth_name_payload(card_name));
return;
}
if (cmd == "notecards")
{
forth_post("/forth/notecards/list", "notecards", "", "{}");
return;
}
if (cmd == "notecard-get" && n == 2)
{
string card_name = llList2String(parts, 1);
forth_post("/forth/notecards/get", "notecard-get", card_name, forth_name_payload(card_name));
return;
}
if (cmd == "notecard-delete" && n == 2)
{
string card_name = llList2String(parts, 1);
forth_post("/forth/notecards/delete", "notecard-delete", card_name, forth_name_payload(card_name));
return;
}
//debug("Unknown command: " + cmd);
}
// =========================
// HTTP RESPONSE
// =========================
http_response(key request_id, integer status, list metadata, string body)
{
string rid = (string)request_id;
integer idx = llListFindList(requests, [rid]);
if (idx == -1)
{
//debug("HTTP response (unknown request): " + (string)status + " body: " + body);
return;
}
string action = llList2String(requests, idx + 1);
string arg = llList2String(requests, idx + 2);
requests = llDeleteSubList(requests, idx, idx + 2);
//say("HTTP response for " + action + " (status " + (string)status + "):");
if (llStringLength(body) == 0)
{
if (action == "bridge") BRIDGE_POLL_IN_FLIGHT = FALSE;
//say("Empty body");
return;
}
if (action == "bridge")
{
BRIDGE_POLL_IN_FLIGHT = FALSE;
string source = llJsonGetValue(body, ["message", "source"]);
if (source != JSON_INVALID)
{
string payload = llJsonSetValue("{}", ["source"], source);
string max_steps = llJsonGetValue(body, ["message", "max_steps"]);
if (max_steps != JSON_INVALID && max_steps != JSON_NULL)
{
payload = llJsonSetValue(payload, ["max_steps"], max_steps);
}
string language = llJsonGetValue(body, ["message", "language"]);
string endpoint = "/forth/eval";
if (language == "ruby") endpoint = "/ruby/eval";
forth_post(endpoint, "forth", (string)CHANNEL_OUTPUT, payload);
}
}
else if (action == "forth")
{
string output = llJsonGetValue(body, ["output"]);
integer reply_channel = (integer)arg;
run_forth_calls(body);
if (output != JSON_INVALID && llStringLength(output) > 0)
{
llSay(reply_channel, output);
}
else
{
llSay(reply_channel, body);
}
}
else if (action == "file-read")
{
string content = llJsonGetValue(body, ["content"]);
integer reply_channel = (integer)arg;
if (content != JSON_INVALID)
{
say_chunks(reply_channel, content);
}
else
{
say_chunks(reply_channel, body);
}
}
else if (action == "algebra")
{
integer reply_channel = (integer)arg;
string simplified = llJsonGetValue(body, ["simplified"]);
string derivative = llJsonGetValue(body, ["derivative"]);
string integral = llJsonGetValue(body, ["integral"]);
if (simplified != JSON_INVALID)
{
say_chunks(reply_channel, "f(x) = " + simplified + " | f'(x) = " + derivative + " | integral = " + integral);
}
else
{
say_chunks(reply_channel, body);
}
}
else if (action == "matrix" || action == "files" || action == "file-write" || action == "file-delete")
{
say_chunks((integer)arg, body);
}
else
{
say_chunks(CHANNEL_OUTPUT, body);
}
if (action == "get")
{
string val = llJsonGetValue(body, [arg]);
if (llStringLength(val) > 0)
{
kv_set(arg, unescape(val));
//debug("Cached " + arg + " = " + val);
}
}
}
timer()
{
bridge_poll();
}
dataserver(key query_id, string data)
{
if (query_id != notecard_query) return;
if (data != EOF)
{
notecard_source += data + "\n";
++notecard_line;
notecard_query = llGetNotecardLine(notecard_name, notecard_line);
return;
}
string json = llJsonSetValue("{}", ["name"], notecard_name);
json = llJsonSetValue(json, ["source"], notecard_source);
forth_post("/forth/notecards/save", "notecard-save", notecard_name, json);
notecard_query = NULL_KEY;
notecard_source = "";
}
}
Tide Route Manual
This manual describes the Tide routes exposed by tiade-maeepers-saerver-all.
The language runtime is bounded by a request step limit, but supports persistent
state, loops, arrays, hashes, mixed Forth/RubyForth execution, named words,
Second Life calls, and isolated sessions.
Conventions
- Base URL:
https://your-server.example - JSON POST routes require
Content-Type: application/json. - Missing
session_iduses the legacydefaultsession. - Session IDs use 1-64 ASCII letters, digits,
_, or-. - LSL defaults to the current prim UUID as its session.
max_stepsdefaults to 10,000 and may be set from 1 through 1,000,000.
Language Routes
| Method | Route | Purpose |
|---|---|---|
| GET | /forth |
Machine-readable language and route catalog. |
| GET | /forth/ui |
Browser console for Forth, RubyForth, algebra, and bridge jobs. |
| GET | /ruby |
RubyForth capability description. |
| POST | /forth/eval |
Execute Forth. Body: source, optional max_steps, optional session_id. |
| POST | /ruby/eval |
Compile and execute RubyForth. Body: source, optional max_steps, optional session_id. |
| POST | /sessions/:session_id/forth/eval |
Execute Forth in the path-selected session. |
| POST | /sessions/:session_id/ruby/eval |
Execute RubyForth in the path-selected session. |
| GET | /sessions/:session_id/status |
Return values visible in one session. |
| POST | /program/run |
Run the lower-level JSON register VM. |
| GET | /program/example |
Return a register-program example. |
| POST | /forth/algebra |
Simplify, differentiate, integrate, and optionally evaluate an integer polynomial. |
| GET | /forth/example |
Return a Forth countdown example. |
Example:
curl -sS https://your-server.example/sessions/workshop_1/ruby/eval \
-H 'Content-Type: application/json' \
--data '{"source":"count = 2; puts count; count += 3; puts count"}'
RubyForth and Forth share the selected session. RubyForth supports semicolon statement separation and multiline Forth blocks:
count = 2; puts count
forth do
$count 3 + count=
$count puts
end
puts count
Variable Routes
These are compatibility routes used by the LSL variable commands.
| Method | Route | Body | Purpose |
|---|---|---|---|
| POST | /vars/set |
JSON values | Set variables. |
| POST | /vars/get |
{"name":"key"} |
Read a variable. |
| POST | /vars/view |
optional action | View variables. |
| POST | /vars/delete |
{"name":"key"} |
Delete a variable. |
| POST | /vars/clear |
optional action | Clear variables. |
| POST | /vars/history |
optional action | Read variable history. |
| POST | /vars/status |
optional action | Read store status. |
For new code, prefer /sessions/:session_id/... language routes so the session
is explicit and cannot be confused with a normal variable name.
Notecards, Matrices, and Files
Notecards
| Method | Route | Purpose |
|---|---|---|
| POST | /forth/notecards/save |
Save name, source, and optional session_id. |
| POST | /forth/notecards/get |
Read a named notecard in the selected session. |
| POST | /forth/notecards/list |
List notecards in the selected session. |
| POST | /forth/notecards/run |
Run a saved Forth notecard in the selected session. |
| POST | /ruby/notecards/run |
Compile and run a saved RubyForth notecard in the selected session. |
| POST | /forth/notecards/delete |
Delete a saved notecard in the selected session. |
Matrices
| Method | Route | Purpose |
|---|---|---|
| POST | /forth/matrices/get |
Read a named persisted matrix. |
Matrices are created and changed by Forth words such as matrix, mset,
m.scale, m.solve, m.transpose, and m.show.
Sandboxed files
| Method | Route | Purpose |
|---|---|---|
| POST | /forth/files/write |
Write a UTF-8 file with name, content, and optional session_id. |
| POST | /forth/files/read |
Read a named file in the selected session. |
| POST | /forth/files/list |
List files in the selected session. |
| POST | /forth/files/delete |
Delete a file in the selected session. |
File names are safe basenames. Files are separate from the partitioned-array language state and are not automatically executable.
Second Life Bridge
| Method | Route | Purpose |
|---|---|---|
| POST | /forth/bridge/enqueue |
Queue Forth or RubyForth using token, source, optional language, optional max_steps, and optional session_id. |
| POST | /forth/bridge/poll |
Authenticated LSL poll; returns one matching queued job. |
| POST | /sl_logger |
Store incoming Second Life log data. |
Set MSSL_FORTH_BRIDGE_TOKEN on the server and the same BRIDGE_TOKEN in the
LSL object. Use /1111 bridge-on to start polling. The LSL client polls
immediately, then on its timer interval, and prevents overlapping polls with
BRIDGE_POLL_IN_FLIGHT.
A bridge job has three stages: queued, delivered, and executed. Delivery removes the job from the queue. If execution fails afterward, this LSL client does not requeue it. Use a session-specific, idempotent program for retryable work.
Static and Utility Routes
| Route | Purpose |
|---|---|
/ |
Main site entry or redirect. |
/ae, /time, /weather |
Legacy utility output. |
/analytics |
Analytics output. |
/bridge/*rest |
Browser bridge pages. |
/chatlog |
Chat-log viewing. |
/css, /fonts, /img, /js, /public |
Static assets. |
/echo |
Echo a request body. |
/file/add, /file/delete |
Legacy temporary-file utilities. |
/flashcard |
Flashcard utility. |
/paema |
Legacy PAEMA handler. |
/parse_plink |
URL parsing utility. |
/praexy-saerver |
Form relay. |
/random, /random2 |
Random utilities. |
/restart-servers |
Persist and signal restart; protect at the proxy layer. |
/rneutri, /rneutrialg |
Legacy text/file routes. |
/rustby |
Rustby integration. |
/sigil-deck and /sigil-deck/* |
Sigil deck cards, uploads, images, and random selection. |
/tiade/img/resize, /tiade/moon, /tiade/sun |
Namespaced legacy Tiade utilities. |
/tiade-maepers/*rest |
Legacy iframe bridge pages. |
Sigil deck subroutes include /sigil-deck/card/:id, /sigil-deck/card/:id/update,
/sigil-deck/card/:id/delete, /sigil-deck/image/:filename,
/sigil-deck/random, and /sigil-deck/upload.
Mounted Compatibility Families
The server also mounts larger compatibility families documented in README.md:
- Admin:
/admin/login,/admin,/admin/add,/admin/remove/:db_name,/admin/delete,/admin/reload,/admin/list,/admin/rehash/:db_name. - Blog:
/blog/login,/blog/signup,/blog/logout, and/blog/:user/*. - Gallery:
/gallery, upload routes, and/gallery/view/:user/*. - Gallery collections:
/gallery/uwu/*and/gallery/owo/*. - Ollama relay:
/ollama,/ollama/health,/chat/:team,/game/:team/:player,/history/:team, and/teams/:team/prompt.
LSL Quick Reference
/1111 help
/1111 channel 0
/1111 session workshop_1
/1111 ruby count = 1; puts count; count += 1; puts count
/1111 forth 2 3 + puts; : square dup * ; 4 square puts;
/1111 bridge-on
/1111 bridge-off
/1111 session-reset
For the internal request ledger, asynchronous HTTP lifecycle, notecard loader, and detailed bridge polling behavior, see LSL_CLIENT_REFERENCE.md.
Battle-Test Checklist
Run these checks against a temporary server or maintenance session before using production data.
Language and session isolation
BASE=https://your-server.example
curl -sS "$BASE/sessions/alpha/forth/eval" -H 'Content-Type: application/json' \
--data '{"source":"7 counter=; $counter puts"}'
curl -sS "$BASE/sessions/beta/forth/eval" -H 'Content-Type: application/json' \
--data '{"source":"$counter puts"}'
curl -sS "$BASE/sessions/alpha/status"
Expected behavior: Alpha reports 7, Beta reports 0, and Alpha status
contains only counter: 7.
Variable, file, and notecard isolation
curl -sS "$BASE/vars/set" -H 'Content-Type: application/json' \
--data '{"session_id":"alpha","note":"alpha-value"}'
curl -sS "$BASE/vars/get" -H 'Content-Type: application/json' \
--data '{"session_id":"beta","name":"note"}'
curl -sS "$BASE/forth/files/write" -H 'Content-Type: application/json' \
--data '{"session_id":"alpha","name":"shared.txt","content":"alpha-file"}'
curl -sS "$BASE/forth/files/read" -H 'Content-Type: application/json' \
--data '{"session_id":"beta","name":"shared.txt"}'
curl -sS "$BASE/forth/notecards/save" -H 'Content-Type: application/json' \
--data '{"session_id":"alpha","name":"CardA","source":"7 puts"}'
curl -sS "$BASE/forth/notecards/get" -H 'Content-Type: application/json' \
--data '{"session_id":"beta","name":"CardA"}'
The Beta variable, file, and notecard reads should each return 404.
Bridge isolation
TOKEN=battle-token
curl -sS "$BASE/forth/bridge/enqueue" -H 'Content-Type: application/json' \
--data "{\"token\":\"$TOKEN\",\"session_id\":\"alpha\",\"language\":\"forth\",\"source\":\"0 \\\"alpha job\\\" sl.owner_say\"}"
curl -sS "$BASE/forth/bridge/enqueue" -H 'Content-Type: application/json' \
--data "{\"token\":\"$TOKEN\",\"session_id\":\"beta\",\"language\":\"forth\",\"source\":\"0 \\\"beta job\\\" sl.owner_say\"}"
curl -sS "$BASE/forth/bridge/poll" -H 'Content-Type: application/json' \
--data "{\"token\":\"$TOKEN\",\"session_id\":\"beta\"}"
curl -sS "$BASE/forth/bridge/poll" -H 'Content-Type: application/json' \
--data "{\"token\":\"$TOKEN\",\"session_id\":\"alpha\"}"
The Beta poll must return the Beta source, and the Alpha poll must return the
Alpha source. An empty matching session returns message: null.
Second Life RustForth Service Guide
Relote.Stack.Service.lsl is an owner-controlled Second Life client for the
Tide/RustForth server. It accepts chat commands, sends HTTPS requests to the
server, and can perform allowlisted server-generated actions in the object that
contains the script.
This guide covers chat commands, Forth, RubyForth, persisted notecards, matrices, files, and the optional browser-to-Second-Life bridge.
For a detailed explanation of the LSL implementation, its event handlers, asynchronous requests, and polling behavior, see LSL_CLIENT_REFERENCE.md.
For the complete HTTP/Tide route reference, see TIDE_ROUTE_MANUAL.md.
Before using real objects or bridge jobs, run the copyable isolation and bridge
checks in the Battle-Test Checklist section of the route manual.
Install and Configure
- Create or select a Second Life object that you own.
- Add
Relote.Stack.Service.lslto the object's inventory. Edit these values at the top of the script:
string SERVERURL = "https://your-server.example"; string BRIDGE_TOKEN = ""; integer CHANNEL_INPUT = 1111; integer CHANNEL_OUTPUT = 0;Save or reset the script. The owner receives a startup message.
Type commands in nearby chat, prefixed with the input channel. With the default configuration, every command begins with
/1111.
Only the object's owner can use this script. Commands from other avatars are ignored.
First Successful Request
Set the output channel to public chat, then run a Forth expression:
/1111 channel 0
/1111 forth 2 3 + .
The first command selects where ordinary results are sent. The second sends
Forth source to POST /forth/eval; . removes the top value from the Forth
stack and includes it in the response output. You should see 5 in public
chat.
Use a nonzero channel when results should not be public:
/1111 channel 42
/1111 forth 6 7 * .
/1111 forth-say 42 10 20 + .
Chat Command Reference
All commands below assume the default input channel 1111.
| Command | Purpose | Example |
|---|---|---|
channel <number> |
Set the normal output channel. | /1111 channel 0 |
help |
Send owner-only syntax help. | /1111 help |
session <name> |
Select a named language session. | /1111 session workshop_1 |
session-reset |
Return to this prim's UUID session. | /1111 session-reset |
forth <source> |
Run a one-line Forth program. | /1111 forth 9 4 - . |
forth-say <channel> <source> |
Run Forth on a chosen channel. | /1111 forth-say 42 2 8 * . |
ruby <source> |
Run one RubyForth statement or expression. | /1111 ruby puts 2 + 3 |
ruby-say <channel> <source> |
Run RubyForth on a chosen channel. | /1111 ruby-say 42 puts "hello" |
algebra <expression> |
Analyze an integer polynomial. | /1111 algebra 3*x^2 - 2*x + 7 |
algebra-say <channel> <expression> |
Send algebra output to a chosen channel. | /1111 algebra-say 42 x^2 + 2*x + 1 |
set <name> <value> |
Store a server variable. | /1111 set greeting hello |
get <name> |
Read a server variable. | /1111 get greeting |
view |
List stored variables. | /1111 view |
delete <name> |
Delete one stored variable. | /1111 delete greeting |
clear |
Clear the server variable store. | /1111 clear |
status |
Request variable-store status. | /1111 status |
history |
Request variable-store history. | /1111 history |
file-write <name> <text> |
Save text in the sandboxed file store. | /1111 file-write note.txt hello |
file-read <name> |
Read a sandboxed file. | /1111 file-read note.txt |
file-read-say <channel> <name> |
Read a file on a chosen channel. | /1111 file-read-say 42 note.txt |
files |
List sandboxed files. | /1111 files |
file-delete <name> |
Delete a sandboxed file. | /1111 file-delete note.txt |
notecard <inventory-name> |
Upload an inventory notecard. | /1111 notecard Counter |
notecards |
List uploaded notecards. | /1111 notecards |
notecard-get <name> |
Fetch an uploaded notecard. | /1111 notecard-get Counter |
notecard-delete <name> |
Delete an uploaded notecard. | /1111 notecard-delete Counter |
forth-run <name> |
Run an uploaded Forth notecard. | /1111 forth-run Counter |
forth-run-say <channel> <name> |
Run a Forth notecard on a chosen channel. | /1111 forth-run-say 42 Counter |
ruby-run <name> |
Compile and run RubyForth. | /1111 ruby-run RubyCounter |
ruby-run-say <channel> <name> |
Run RubyForth on a chosen channel. | /1111 ruby-run-say 42 RubyCounter |
matrix-get <name> |
Read a persisted matrix. | /1111 matrix-get A |
matrix-get-say <channel> <name> |
Read a matrix on a chosen channel. | /1111 matrix-get-say 42 A |
bridge-on |
Start polling for browser-queued bridge jobs. | /1111 bridge-on |
bridge-off |
Stop bridge polling. | /1111 bridge-off |
bridge-poll |
Trigger one bridge poll immediately. | /1111 bridge-poll |
The command parser splits on spaces. Use chat commands for short, single-line source and use notecards for multiline Forth or RubyForth.
Server Variables
The set, get, view, delete, clear, status, and history commands
use the server variable store. Values are server-persistent; the kv list in
the LSL script is only a local response cache.
/1111 set score 125
/1111 get score
/1111 view
/1111 delete score
Avoid backslashes and these characters in names or values: :, [, ], {,
}, single quotes, double quotes, and periods. The current LSL escape/unescape
implementation does not round-trip those characters correctly. Plain names such
as score, player_name, and simple text values are safe.
Forth Basics
Forth is stack-based. Numbers and strings are pushed onto the stack. Most words pop their inputs from the stack and push a result.
2 3 + .
This pushes 2, pushes 3, adds them, then prints 5.
| Group | Words |
|---|---|
| Arithmetic | +, -, *, /, mod, abs, min, max |
| Comparisons | =, !=, <, <=, >, >=, 0= |
| Boolean | true, false, nil, not, and, or |
| Stack | dup, drop, swap, over |
| Output | ., puts, p |
| Time/randomness | now, rand |
| Control | if ... else ... then, begin ... until, begin ... again, bye |
| Variables | let, variable, @, !, $name, name=, name+=, name-= |
| Named words | : name ... ; |
Numbers are signed integers. / is integer division. Strings use double
quotes; a backslash begins a Forth comment outside a string.
Variables
Variables persist in the same server store used by Forth, RubyForth, matrices, and notecards.
let total
25 total !
$total 5 + total!
$total puts
total pushes a variable address for ! and @. $total pushes its current
value. The concise forms total!, total+=, and total-= assign, add, and
subtract respectively.
Named Forth Words
You can give a sequence of Forth tokens a name and use that name later in the same program or notecard. This is the main new tool for breaking a larger program into reusable pieces.
: square dup * ;
: announce-square square puts ;
6 announce-square
Read the first definition as: βdefine the word square; when it is used, copy
the top stack value with dup, multiply the copy and original with *, then
finish the definition at ;.β The second definition calls the first, then
prints its result. The final line pushes 6 and calls announce-square, which
prints 36.
Definitions are collected before execution, so a word may call another word
defined later in the same source. They are expanded before the interpreter runs,
which keeps existing loops and safety limits unchanged. Recursive definitions,
such as : forever forever ;, are rejected. Do not reuse the name of a built-in
word such as if, puts, or matrix.
Semicolon-Separated Forth
Outside a : name ... ; definition, a semicolon is a statement separator. This
is useful in one-line chat commands:
/1111 forth 2 3 + puts; : square dup * ; 4 square puts;
The output is 5 16. The semicolon after square still closes the definition;
the other semicolons simply separate sequential blocks.
For a short definition in chat:
/1111 forth : square dup * ; 9 square puts
Use a notecard for multi-line definitions.
Explicit Execution Budgets
The server defaults to 10,000 interpreter steps and accepts at most 1,000,000. Long loops now have direct LSL commands that set this budget explicitly:
/1111 forth-steps 100000 : square dup * ; 250 square puts
/1111 ruby-steps 100000 puts 2 + 3
/1111 forth-run-steps 100000 Counter
/1111 ruby-run-steps 100000 RubyCounter
The second argument to each *-steps command must be an integer from 1 to
1000000. A larger budget permits more computation, but does not remove the
server's resource limit.
Forth and RubyForth in One Program
The ruby Forth directive compiles its quoted RubyForth source into the
surrounding Forth program. The compiled code uses the same variables, named
words, stack, output response, and allowed Second Life calls as the containing
Forth source.
: square dup * ;
6 square puts
ruby "count = 4\nputs count * 3"
This prints 36 from Forth, then 12 from RubyForth. The \n is a newline
inside the quoted RubyForth source. In a notecard, a multiline Ruby snippet is
also allowed inside the quoted string.
RubyForth can send a quoted Forth source string back through the same expander:
forth ": double dup + ; 21 double puts"
puts "RubyForth continues after Forth"
The first line defines and calls a Forth word, printing 42; the second line
is ordinary RubyForth and prints its text afterward. A forth bridge statement
must contain exactly one quoted Forth source string.
For nearby chat, use the new Forth-originated alias:
/1111 hybrid : square dup * ; 6 square puts ruby "puts 2 + 3"
/1111 hybrid-say 42 ruby "puts 6 * 7"
hybrid uses /forth/eval, so it can start with Forth and include one or more
ruby "..." directives. Regular /1111 forth accepts the same bridge syntax;
hybrid only makes that intent explicit in chat.
Forth Notecard Examples
Create a notecard with the exact inventory name shown, paste its contents, place it in the same object as the script, and upload it with:
/1111 notecard <NotecardName>
Wait for the object to report that it is loading the notecard, then run it with
/1111 forth-run <NotecardName>.
What a notecard is and what happens to it
A notecard is an inventory item inside the Second Life object. It is not a file on your computer, and it is not automatically a program. The script reads it one line at a time, sends its text to the server, and stores a server-side copy under the same name.
Use this exact sequence for every example:
- Create a new notecard in the object's inventory and name it exactly as the
example heading says. For example, name the first one
Counter. - Open the notecard and paste only the code inside its matching code block. Do not paste the Markdown backticks, the heading, or explanatory prose.
- Save the notecard and close it.
- In nearby chat, as the object owner, enter
/1111 notecard Counter. - The LSL script asks Second Life for each line. When it reaches the end, it
uploads the complete text to the server as the remote program named
Counter. - Wait for the loading message to appear. Large notecards can take longer than one chat message because Second Life returns their lines asynchronously.
- Run the uploaded copy with
/1111 forth-run Counter. - Read the result on the configured output channel. Set that channel first
with
/1111 channel 0if you want public-chat output.
The inventory name and the command argument are case-sensitive in practice:
use the same spelling every time. Editing the local notecard does not update the
server copy. Run /1111 notecard Counter again after every edit, then rerun
/1111 forth-run Counter.
forth-run executes source as Forth. ruby-run executes the same uploaded
source as RubyForth. Do not run a Forth example with ruby-run, and do not run
a RubyForth example with forth-run.
Counter
let counter
3 counter !
begin
$counter puts
1 counter-=
$counter 0=
until
Expected output: 3, 2, 1.
Condition
let temperature
23 temperature !
$temperature 20 > if
"warm" puts
else
"cool" puts
then
StackTools
10 20 over . . .
over copies the second stack item. This example outputs 10, 20, then
10.
RandomNumber
100 rand puts
Outputs an integer from 0 through 99.
Greeting
"Hello, Second Life" puts
"Hello, " "RustForth" + puts
MatrixDeterminant
2 2 matrix A
1 0 0 mset A
2 0 1 mset A
3 1 0 mset A
4 1 1 mset A
m.show A
m.det A puts
This creates a two-by-two matrix and prints its serialized form followed by
determinant -2.
MatrixScale
2 m.identity I
5 m.scale I FiveI
m.show FiveI
MatrixSolve
2 2 matrix A
2 0 0 mset A
0 0 1 mset A
2 1 0 mset A
1 1 1 mset A
2 1 matrix B
4 0 0 mset B
6 1 0 mset B
m.solve A B X
m.show X
This solves $A X = B$ when the solution contains exact integers.
Matrix Reference
Matrices persist server-side. The maximum dimension is 64 and the maximum
number of cells is 4096.
| Word | Stack/source form | Result |
|---|---|---|
matrix |
<rows> <cols> matrix <name> |
Create a zero matrix. |
m.identity |
<size> m.identity <name> |
Create an identity matrix. |
mset |
<value> <row> <col> mset <name> |
Set one cell. |
mget |
<row> <col> mget <name> |
Push one cell. |
m.fill |
<value> m.fill <name> |
Fill every cell. |
m.scale |
<scalar> m.scale <source> <target> |
Scale a matrix. |
m.add |
m.add <left> <right> <target> |
Add matrices. |
m.sub |
m.sub <left> <right> <target> |
Subtract matrices. |
m.mul |
m.mul <left> <right> <target> |
Multiply matrices. |
m.solve |
m.solve <left> <right> <target> |
Solve $A X = B$ with exact integers. |
m.transpose |
m.transpose <source> <target> |
Transpose a matrix. |
m.det |
m.det <name> |
Push determinant. |
m.rows |
m.rows <name> |
Push row count. |
m.cols |
m.cols <name> |
Push column count. |
m.show |
m.show <name> |
Emit serialized matrix output. |
Use /1111 matrix-get <name> to retrieve a stored matrix outside a Forth
program.
Forth-Controlled Second Life Actions
The Forth runtime does not directly run arbitrary LSL. Instead, it returns an
allowlisted calls list, and this script performs each call in the object.
These actions work with forth, forth-run, and Forth bridge jobs. At most 128
host calls are allowed in one Forth program.
The action is performed when this LSL client receives the evaluation response,
so a direct command such as /1111 forth ... can cause the action immediately.
Speak in chat
0 "Hello from the object" sl.say
0 "A quiet hello" sl.whisper
0 "Attention" sl.shout
42 "Private channel message" sl.region_say
"Only the owner sees this" sl.owner_say
The stack order for the first four forms is <channel> <message>.
Change floating text, color, and alpha
"Ready" 0 255 0 255 sl.set_text
255 0 0 -1 sl.set_color
128 -1 sl.set_alpha
Color and alpha components accept either 0 through 1 or 0 through 255.
-1 means all faces for sl.set_color and sl.set_alpha.
Play a sound
Replace the UUID with a sound asset UUID in the object's inventory or a sound that the object is permitted to play:
"01234567-89ab-cdef-0123-456789abcdef" 1 sl.play_sound
The stack order is <sound> <volume>.
Object timer, position, and linked messages
5 sl.set_timer
128 128 25 sl.set_region_pos
sl.set_timer invokes llSetTimerEvent; it does not add custom timer handling
to this script. The existing timer event is used for bridge polling, so do not
use sl.set_timer while bridge polling is enabled unless changing the poll
interval is intentional.
sl.set_region_pos requires the usual Second Life permissions and parcel
conditions. sl.link_message has stack order <link> <code> <message> <id>;
-1 is LINK_SET.
ObjectWelcome
"RustForth online" 0 200 100 255 sl.set_text
0 "Object is ready" sl.say
"Object is ready" sl.owner_say
PulseColor
0 128 255 -1 sl.set_color
255 -1 sl.set_alpha
0 "Color changed" sl.whisper
RubyForth
RubyForth is a small Ruby-shaped language compiled into the Forth runtime. It
shares persistent variables, arrays, hashes, matrices, notecards, and execution
budgets with Forth, but it is not MRI Ruby. It has no gems, require, eval,
classes, arbitrary host methods, arbitrary file access, or system commands.
Supported constructs are integer and string expressions, #{name}
interpolation, assignment, +=, -=, *=, /=, puts, p, if,
unless, elsif, else, while, until, n.times do, end, return,
one-line arrays/hashes, and collection helpers such as get, set, push,
delete, keys, length, and has.
Semicolons separate RubyForth statements outside strings, arrays, hashes, and parentheses. This makes multi-step direct chat commands possible:
/1111 ruby count = 1; puts count; count += 1; puts count
Use a notecard for multiline blocks, not merely because of semicolons.
/1111 ruby puts 2 + 3
/1111 ruby puts "Hello from RubyForth"
/1111 ruby-say 42 puts 6 * 7
RubyForth currently cannot call sl.say, sl.set_text, or any other sl.*
operation. Use a Forth notecard when the program must change the in-world
object.
Mixing RubyForth and Forth
Use a quoted one-line bridge when the Forth fragment is short:
count = 2
forth "$count 3 + count="
puts count
For a larger Forth fragment, use a multiline forth do ... end block:
count = 2; puts count
forth do
$count 3 + count=
$count puts
end
puts count
This emits 2 5 5: RubyForth initializes and prints count, Forth adds three
and prints it, and RubyForth reads the same persisted value afterward. Forth
inside this block can use named words, matrices, arrays, hashes, or allowed
sl.* calls. RubyForth resumes after the block using the same execution budget.
Forth can call a short RubyForth fragment in the other direction:
ruby "count = 4; puts count * 3"
Per-Prim Sessions
At startup, the LSL client sets its language session to the current prim UUID. That means Forth/RubyForth state from two objects using the same server can be isolated. Change the active scope only when you intentionally want to share state:
/1111 session workshop_1
/1111 ruby score = 10; puts score
/1111 session-reset
Session names must use letters, numbers, _, or -, and be at most 64
characters. Existing API callers that omit session_id keep using the legacy
default session.
RubyForth Notecard Examples
Upload each notecard with /1111 notecard <NotecardName>, then run it with
/1111 ruby-run <NotecardName>.
RubyCounter
count = 0
while count < 3
puts count
count += 1
end
Expected output: 0, 1, 2.
RubyTimes
5.times do
puts "tick"
end
RubyCondition
score = 80
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "Needs work"
end
RubyUnless
enabled = false
unless enabled
puts "Feature is disabled"
end
RubyArithmetic
total = 4
total *= 3
total += 2
puts total
Expected output: 14.
RubyStrings
first = "Second"
second = "Life"
puts first + " " + second
RubyUntil
remaining = 3
until remaining == 0
puts remaining
remaining -= 1
end
Files and Notecard Management
The server has two separate persistence mechanisms:
- Notecards store source programs in the partitioned server store. Upload
inventory notecards with
notecard; run them withforth-runorruby-run. - Files are UTF-8 text documents in a sandboxed file store. Manage them
with
file-write,file-read,files, andfile-delete. They are not executable unless their contents are submitted as program source.
/1111 file-write ideas.txt build a counter notecard
/1111 file-read ideas.txt
/1111 files
/1111 file-delete ideas.txt
/1111 notecard Counter
/1111 notecards
/1111 forth-run Counter
/1111 notecard-delete Counter
Browser-to-Second-Life Bridge
The bridge lets the browser console queue Forth or RubyForth source. A Second Life object with this script polls the queue and evaluates one queued job at a time.
Server Setup
Set a nonempty secret before starting the server:
export MSSL_FORTH_BRIDGE_TOKEN='replace-with-a-long-random-token'
Set that exact same value in the script:
string BRIDGE_TOKEN = "replace-with-a-long-random-token";
Do not publish the token in a public script. Anyone who knows it can enqueue jobs for a bridge-enabled object.
Object Setup
After saving or resetting the configured script:
/1111 bridge-on
The object polls every three seconds. Stop polling with:
/1111 bridge-off
bridge-poll sends one immediate poll request, which is useful while
diagnosing connectivity.
Queue a Job
Open https://your-server.example/forth/ui. Paste Forth source, enter the
bridge token, and choose Queue Forth for SL. For example:
"Queued job reached Second Life" sl.owner_say
Use Queue Ruby for SL only for RubyForth computation or output. RubyForth
does not currently produce sl.* calls, so it will not modify the in-world
object.
Troubleshooting
| Symptom | Check |
|---|---|
| No startup message | Confirm the object contains a saved script and inspect the Second Life script error window. |
| No command response | Use /1111, confirm you own the object, and verify SERVERURL is reachable over HTTPS. |
| Output appears in the wrong place | Send /1111 channel 0, or use a *-say command with an explicit channel. |
| Forth reports stack underflow | Supply the word's inputs. For sl.say, push channel and then message. |
| Ruby parsing fails | Use one statement per line, omit semicolons, and use only the documented RubyForth subset. |
| Notecard upload fails | Ensure the notecard is in the same object's inventory and its name exactly matches the command. |
| Bridge refuses to start | Set a nonempty BRIDGE_TOKEN in the script. |
| Bridge receives no job | Ensure the server and script use the same MSSL_FORTH_BRIDGE_TOKEN, then use /1111 bridge-poll. |
| Variable text is malformed | Avoid special characters listed in the variable warning until escaping is corrected. |
| Object action is ignored | Confirm it came from Forth rather than RubyForth, and check normal Second Life permissions and parcel restrictions. |
Practical Workflow
- Use
/1111 forth ...for quick arithmetic or a short test. - Move multiline logic into a named inventory notecard.
- Upload with
/1111 notecard Nameafter every notecard edit. - Use
forth-runfor object actions andruby-runfor compact control-flow programs and server-side calculations. - Enable the bridge only when browser-queued work must reach a specific in-world object.
RubyForth Guide
Second Life RustForth Service Guide
Relote.Stack.Service.lsl is an owner-controlled Second Life client for the
Tide/RustForth server. It accepts chat commands, sends HTTPS requests to the
server, and can perform allowlisted server-generated actions in the object that
contains the script.
This guide covers chat commands, Forth, RubyForth, persisted notecards, matrices, files, and the optional browser-to-Second-Life bridge.
For a detailed explanation of the LSL implementation, its event handlers, asynchronous requests, and polling behavior, see LSL_CLIENT_REFERENCE.md.
For the complete HTTP/Tide route reference, see TIDE_ROUTE_MANUAL.md.
Before using real objects or bridge jobs, run the copyable isolation and bridge
checks in the Battle-Test Checklist section of the route manual.
Install and Configure
- Create or select a Second Life object that you own.
- Add
Relote.Stack.Service.lslto the object's inventory. Edit these values at the top of the script:
string SERVERURL = "https://your-server.example"; string BRIDGE_TOKEN = ""; integer CHANNEL_INPUT = 1111; integer CHANNEL_OUTPUT = 0;Save or reset the script. The owner receives a startup message.
Type commands in nearby chat, prefixed with the input channel. With the default configuration, every command begins with
/1111.
Only the object's owner can use this script. Commands from other avatars are ignored.
First Successful Request
Set the output channel to public chat, then run a Forth expression:
/1111 channel 0
/1111 forth 2 3 + .
The first command selects where ordinary results are sent. The second sends
Forth source to POST /forth/eval; . removes the top value from the Forth
stack and includes it in the response output. You should see 5 in public
chat.
Use a nonzero channel when results should not be public:
/1111 channel 42
/1111 forth 6 7 * .
/1111 forth-say 42 10 20 + .
Chat Command Reference
All commands below assume the default input channel 1111.
| Command | Purpose | Example |
|---|---|---|
channel <number> |
Set the normal output channel. | /1111 channel 0 |
help |
Send owner-only syntax help. | /1111 help |
session <name> |
Select a named language session. | /1111 session workshop_1 |
session-reset |
Return to this prim's UUID session. | /1111 session-reset |
forth <source> |
Run a one-line Forth program. | /1111 forth 9 4 - . |
forth-say <channel> <source> |
Run Forth on a chosen channel. | /1111 forth-say 42 2 8 * . |
ruby <source> |
Run one RubyForth statement or expression. | /1111 ruby puts 2 + 3 |
ruby-say <channel> <source> |
Run RubyForth on a chosen channel. | /1111 ruby-say 42 puts "hello" |
algebra <expression> |
Analyze an integer polynomial. | /1111 algebra 3*x^2 - 2*x + 7 |
algebra-say <channel> <expression> |
Send algebra output to a chosen channel. | /1111 algebra-say 42 x^2 + 2*x + 1 |
set <name> <value> |
Store a server variable. | /1111 set greeting hello |
get <name> |
Read a server variable. | /1111 get greeting |
view |
List stored variables. | /1111 view |
delete <name> |
Delete one stored variable. | /1111 delete greeting |
clear |
Clear the server variable store. | /1111 clear |
status |
Request variable-store status. | /1111 status |
history |
Request variable-store history. | /1111 history |
file-write <name> <text> |
Save text in the sandboxed file store. | /1111 file-write note.txt hello |
file-read <name> |
Read a sandboxed file. | /1111 file-read note.txt |
file-read-say <channel> <name> |
Read a file on a chosen channel. | /1111 file-read-say 42 note.txt |
files |
List sandboxed files. | /1111 files |
file-delete <name> |
Delete a sandboxed file. | /1111 file-delete note.txt |
notecard <inventory-name> |
Upload an inventory notecard. | /1111 notecard Counter |
notecards |
List uploaded notecards. | /1111 notecards |
notecard-get <name> |
Fetch an uploaded notecard. | /1111 notecard-get Counter |
notecard-delete <name> |
Delete an uploaded notecard. | /1111 notecard-delete Counter |
forth-run <name> |
Run an uploaded Forth notecard. | /1111 forth-run Counter |
forth-run-say <channel> <name> |
Run a Forth notecard on a chosen channel. | /1111 forth-run-say 42 Counter |
ruby-run <name> |
Compile and run RubyForth. | /1111 ruby-run RubyCounter |
ruby-run-say <channel> <name> |
Run RubyForth on a chosen channel. | /1111 ruby-run-say 42 RubyCounter |
matrix-get <name> |
Read a persisted matrix. | /1111 matrix-get A |
matrix-get-say <channel> <name> |
Read a matrix on a chosen channel. | /1111 matrix-get-say 42 A |
bridge-on |
Start polling for browser-queued bridge jobs. | /1111 bridge-on |
bridge-off |
Stop bridge polling. | /1111 bridge-off |
bridge-poll |
Trigger one bridge poll immediately. | /1111 bridge-poll |
The command parser splits on spaces. Use chat commands for short, single-line source and use notecards for multiline Forth or RubyForth.
Server Variables
The set, get, view, delete, clear, status, and history commands
use the server variable store. Values are server-persistent; the kv list in
the LSL script is only a local response cache.
/1111 set score 125
/1111 get score
/1111 view
/1111 delete score
Avoid backslashes and these characters in names or values: :, [, ], {,
}, single quotes, double quotes, and periods. The current LSL escape/unescape
implementation does not round-trip those characters correctly. Plain names such
as score, player_name, and simple text values are safe.
Forth Basics
Forth is stack-based. Numbers and strings are pushed onto the stack. Most words pop their inputs from the stack and push a result.
2 3 + .
This pushes 2, pushes 3, adds them, then prints 5.
| Group | Words |
|---|---|
| Arithmetic | +, -, *, /, mod, abs, min, max |
| Comparisons | =, !=, <, <=, >, >=, 0= |
| Boolean | true, false, nil, not, and, or |
| Stack | dup, drop, swap, over |
| Output | ., puts, p |
| Time/randomness | now, rand |
| Control | if ... else ... then, begin ... until, begin ... again, bye |
| Variables | let, variable, @, !, $name, name=, name+=, name-= |
| Named words | : name ... ; |
Numbers are signed integers. / is integer division. Strings use double
quotes; a backslash begins a Forth comment outside a string.
Variables
Variables persist in the same server store used by Forth, RubyForth, matrices, and notecards.
let total
25 total !
$total 5 + total!
$total puts
total pushes a variable address for ! and @. $total pushes its current
value. The concise forms total!, total+=, and total-= assign, add, and
subtract respectively.
Named Forth Words
You can give a sequence of Forth tokens a name and use that name later in the same program or notecard. This is the main new tool for breaking a larger program into reusable pieces.
: square dup * ;
: announce-square square puts ;
6 announce-square
Read the first definition as: βdefine the word square; when it is used, copy
the top stack value with dup, multiply the copy and original with *, then
finish the definition at ;.β The second definition calls the first, then
prints its result. The final line pushes 6 and calls announce-square, which
prints 36.
Definitions are collected before execution, so a word may call another word
defined later in the same source. They are expanded before the interpreter runs,
which keeps existing loops and safety limits unchanged. Recursive definitions,
such as : forever forever ;, are rejected. Do not reuse the name of a built-in
word such as if, puts, or matrix.
Semicolon-Separated Forth
Outside a : name ... ; definition, a semicolon is a statement separator. This
is useful in one-line chat commands:
/1111 forth 2 3 + puts; : square dup * ; 4 square puts;
The output is 5 16. The semicolon after square still closes the definition;
the other semicolons simply separate sequential blocks.
For a short definition in chat:
/1111 forth : square dup * ; 9 square puts
Use a notecard for multi-line definitions.
Explicit Execution Budgets
The server defaults to 10,000 interpreter steps and accepts at most 1,000,000. Long loops now have direct LSL commands that set this budget explicitly:
/1111 forth-steps 100000 : square dup * ; 250 square puts
/1111 ruby-steps 100000 puts 2 + 3
/1111 forth-run-steps 100000 Counter
/1111 ruby-run-steps 100000 RubyCounter
The second argument to each *-steps command must be an integer from 1 to
1000000. A larger budget permits more computation, but does not remove the
server's resource limit.
Forth and RubyForth in One Program
The ruby Forth directive compiles its quoted RubyForth source into the
surrounding Forth program. The compiled code uses the same variables, named
words, stack, output response, and allowed Second Life calls as the containing
Forth source.
: square dup * ;
6 square puts
ruby "count = 4\nputs count * 3"
This prints 36 from Forth, then 12 from RubyForth. The \n is a newline
inside the quoted RubyForth source. In a notecard, a multiline Ruby snippet is
also allowed inside the quoted string.
RubyForth can send a quoted Forth source string back through the same expander:
forth ": double dup + ; 21 double puts"
puts "RubyForth continues after Forth"
The first line defines and calls a Forth word, printing 42; the second line
is ordinary RubyForth and prints its text afterward. A forth bridge statement
must contain exactly one quoted Forth source string.
For nearby chat, use the new Forth-originated alias:
/1111 hybrid : square dup * ; 6 square puts ruby "puts 2 + 3"
/1111 hybrid-say 42 ruby "puts 6 * 7"
hybrid uses /forth/eval, so it can start with Forth and include one or more
ruby "..." directives. Regular /1111 forth accepts the same bridge syntax;
hybrid only makes that intent explicit in chat.
Forth Notecard Examples
Create a notecard with the exact inventory name shown, paste its contents, place it in the same object as the script, and upload it with:
/1111 notecard <NotecardName>
Wait for the object to report that it is loading the notecard, then run it with
/1111 forth-run <NotecardName>.
What a notecard is and what happens to it
A notecard is an inventory item inside the Second Life object. It is not a file on your computer, and it is not automatically a program. The script reads it one line at a time, sends its text to the server, and stores a server-side copy under the same name.
Use this exact sequence for every example:
- Create a new notecard in the object's inventory and name it exactly as the
example heading says. For example, name the first one
Counter. - Open the notecard and paste only the code inside its matching code block. Do not paste the Markdown backticks, the heading, or explanatory prose.
- Save the notecard and close it.
- In nearby chat, as the object owner, enter
/1111 notecard Counter. - The LSL script asks Second Life for each line. When it reaches the end, it
uploads the complete text to the server as the remote program named
Counter. - Wait for the loading message to appear. Large notecards can take longer than one chat message because Second Life returns their lines asynchronously.
- Run the uploaded copy with
/1111 forth-run Counter. - Read the result on the configured output channel. Set that channel first
with
/1111 channel 0if you want public-chat output.
The inventory name and the command argument are case-sensitive in practice:
use the same spelling every time. Editing the local notecard does not update the
server copy. Run /1111 notecard Counter again after every edit, then rerun
/1111 forth-run Counter.
forth-run executes source as Forth. ruby-run executes the same uploaded
source as RubyForth. Do not run a Forth example with ruby-run, and do not run
a RubyForth example with forth-run.
Counter
let counter
3 counter !
begin
$counter puts
1 counter-=
$counter 0=
until
Expected output: 3, 2, 1.
Condition
let temperature
23 temperature !
$temperature 20 > if
"warm" puts
else
"cool" puts
then
StackTools
10 20 over . . .
over copies the second stack item. This example outputs 10, 20, then
10.
RandomNumber
100 rand puts
Outputs an integer from 0 through 99.
Greeting
"Hello, Second Life" puts
"Hello, " "RustForth" + puts
MatrixDeterminant
2 2 matrix A
1 0 0 mset A
2 0 1 mset A
3 1 0 mset A
4 1 1 mset A
m.show A
m.det A puts
This creates a two-by-two matrix and prints its serialized form followed by
determinant -2.
MatrixScale
2 m.identity I
5 m.scale I FiveI
m.show FiveI
MatrixSolve
2 2 matrix A
2 0 0 mset A
0 0 1 mset A
2 1 0 mset A
1 1 1 mset A
2 1 matrix B
4 0 0 mset B
6 1 0 mset B
m.solve A B X
m.show X
This solves $A X = B$ when the solution contains exact integers.
Matrix Reference
Matrices persist server-side. The maximum dimension is 64 and the maximum
number of cells is 4096.
| Word | Stack/source form | Result |
|---|---|---|
matrix |
<rows> <cols> matrix <name> |
Create a zero matrix. |
m.identity |
<size> m.identity <name> |
Create an identity matrix. |
mset |
<value> <row> <col> mset <name> |
Set one cell. |
mget |
<row> <col> mget <name> |
Push one cell. |
m.fill |
<value> m.fill <name> |
Fill every cell. |
m.scale |
<scalar> m.scale <source> <target> |
Scale a matrix. |
m.add |
m.add <left> <right> <target> |
Add matrices. |
m.sub |
m.sub <left> <right> <target> |
Subtract matrices. |
m.mul |
m.mul <left> <right> <target> |
Multiply matrices. |
m.solve |
m.solve <left> <right> <target> |
Solve $A X = B$ with exact integers. |
m.transpose |
m.transpose <source> <target> |
Transpose a matrix. |
m.det |
m.det <name> |
Push determinant. |
m.rows |
m.rows <name> |
Push row count. |
m.cols |
m.cols <name> |
Push column count. |
m.show |
m.show <name> |
Emit serialized matrix output. |
Use /1111 matrix-get <name> to retrieve a stored matrix outside a Forth
program.
Forth-Controlled Second Life Actions
The Forth runtime does not directly run arbitrary LSL. Instead, it returns an
allowlisted calls list, and this script performs each call in the object.
These actions work with forth, forth-run, and Forth bridge jobs. At most 128
host calls are allowed in one Forth program.
The action is performed when this LSL client receives the evaluation response,
so a direct command such as /1111 forth ... can cause the action immediately.
Speak in chat
0 "Hello from the object" sl.say
0 "A quiet hello" sl.whisper
0 "Attention" sl.shout
42 "Private channel message" sl.region_say
"Only the owner sees this" sl.owner_say
The stack order for the first four forms is <channel> <message>.
Change floating text, color, and alpha
"Ready" 0 255 0 255 sl.set_text
255 0 0 -1 sl.set_color
128 -1 sl.set_alpha
Color and alpha components accept either 0 through 1 or 0 through 255.
-1 means all faces for sl.set_color and sl.set_alpha.
Play a sound
Replace the UUID with a sound asset UUID in the object's inventory or a sound that the object is permitted to play:
"01234567-89ab-cdef-0123-456789abcdef" 1 sl.play_sound
The stack order is <sound> <volume>.
Object timer, position, and linked messages
5 sl.set_timer
128 128 25 sl.set_region_pos
sl.set_timer invokes llSetTimerEvent; it does not add custom timer handling
to this script. The existing timer event is used for bridge polling, so do not
use sl.set_timer while bridge polling is enabled unless changing the poll
interval is intentional.
sl.set_region_pos requires the usual Second Life permissions and parcel
conditions. sl.link_message has stack order <link> <code> <message> <id>;
-1 is LINK_SET.
ObjectWelcome
"RustForth online" 0 200 100 255 sl.set_text
0 "Object is ready" sl.say
"Object is ready" sl.owner_say
PulseColor
0 128 255 -1 sl.set_color
255 -1 sl.set_alpha
0 "Color changed" sl.whisper
RubyForth
RubyForth is a small Ruby-shaped language compiled into the Forth runtime. It
shares persistent variables, arrays, hashes, matrices, notecards, and execution
budgets with Forth, but it is not MRI Ruby. It has no gems, require, eval,
classes, arbitrary host methods, arbitrary file access, or system commands.
Supported constructs are integer and string expressions, #{name}
interpolation, assignment, +=, -=, *=, /=, puts, p, if,
unless, elsif, else, while, until, n.times do, end, return,
one-line arrays/hashes, and collection helpers such as get, set, push,
delete, keys, length, and has.
Semicolons separate RubyForth statements outside strings, arrays, hashes, and parentheses. This makes multi-step direct chat commands possible:
/1111 ruby count = 1; puts count; count += 1; puts count
Use a notecard for multiline blocks, not merely because of semicolons.
/1111 ruby puts 2 + 3
/1111 ruby puts "Hello from RubyForth"
/1111 ruby-say 42 puts 6 * 7
RubyForth currently cannot call sl.say, sl.set_text, or any other sl.*
operation. Use a Forth notecard when the program must change the in-world
object.
Mixing RubyForth and Forth
Use a quoted one-line bridge when the Forth fragment is short:
count = 2
forth "$count 3 + count="
puts count
For a larger Forth fragment, use a multiline forth do ... end block:
count = 2; puts count
forth do
$count 3 + count=
$count puts
end
puts count
This emits 2 5 5: RubyForth initializes and prints count, Forth adds three
and prints it, and RubyForth reads the same persisted value afterward. Forth
inside this block can use named words, matrices, arrays, hashes, or allowed
sl.* calls. RubyForth resumes after the block using the same execution budget.
Forth can call a short RubyForth fragment in the other direction:
ruby "count = 4; puts count * 3"
Per-Prim Sessions
At startup, the LSL client sets its language session to the current prim UUID. That means Forth/RubyForth state from two objects using the same server can be isolated. Change the active scope only when you intentionally want to share state:
/1111 session workshop_1
/1111 ruby score = 10; puts score
/1111 session-reset
Session names must use letters, numbers, _, or -, and be at most 64
characters. Existing API callers that omit session_id keep using the legacy
default session.
RubyForth Notecard Examples
Upload each notecard with /1111 notecard <NotecardName>, then run it with
/1111 ruby-run <NotecardName>.
RubyCounter
count = 0
while count < 3
puts count
count += 1
end
Expected output: 0, 1, 2.
RubyTimes
5.times do
puts "tick"
end
RubyCondition
score = 80
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "Needs work"
end
RubyUnless
enabled = false
unless enabled
puts "Feature is disabled"
end
RubyArithmetic
total = 4
total *= 3
total += 2
puts total
Expected output: 14.
RubyStrings
first = "Second"
second = "Life"
puts first + " " + second
RubyUntil
remaining = 3
until remaining == 0
puts remaining
remaining -= 1
end
Files and Notecard Management
The server has two separate persistence mechanisms:
- Notecards store source programs in the partitioned server store. Upload
inventory notecards with
notecard; run them withforth-runorruby-run. - Files are UTF-8 text documents in a sandboxed file store. Manage them
with
file-write,file-read,files, andfile-delete. They are not executable unless their contents are submitted as program source.
/1111 file-write ideas.txt build a counter notecard
/1111 file-read ideas.txt
/1111 files
/1111 file-delete ideas.txt
/1111 notecard Counter
/1111 notecards
/1111 forth-run Counter
/1111 notecard-delete Counter
Browser-to-Second-Life Bridge
The bridge lets the browser console queue Forth or RubyForth source. A Second Life object with this script polls the queue and evaluates one queued job at a time.
Server Setup
Set a nonempty secret before starting the server:
export MSSL_FORTH_BRIDGE_TOKEN='replace-with-a-long-random-token'
Set that exact same value in the script:
string BRIDGE_TOKEN = "replace-with-a-long-random-token";
Do not publish the token in a public script. Anyone who knows it can enqueue jobs for a bridge-enabled object.
Object Setup
After saving or resetting the configured script:
/1111 bridge-on
The object polls every three seconds. Stop polling with:
/1111 bridge-off
bridge-poll sends one immediate poll request, which is useful while
diagnosing connectivity.
Queue a Job
Open https://your-server.example/forth/ui. Paste Forth source, enter the
bridge token, and choose Queue Forth for SL. For example:
"Queued job reached Second Life" sl.owner_say
Use Queue Ruby for SL only for RubyForth computation or output. RubyForth
does not currently produce sl.* calls, so it will not modify the in-world
object.
Troubleshooting
| Symptom | Check |
|---|---|
| No startup message | Confirm the object contains a saved script and inspect the Second Life script error window. |
| No command response | Use /1111, confirm you own the object, and verify SERVERURL is reachable over HTTPS. |
| Output appears in the wrong place | Send /1111 channel 0, or use a *-say command with an explicit channel. |
| Forth reports stack underflow | Supply the word's inputs. For sl.say, push channel and then message. |
| Ruby parsing fails | Use one statement per line, omit semicolons, and use only the documented RubyForth subset. |
| Notecard upload fails | Ensure the notecard is in the same object's inventory and its name exactly matches the command. |
| Bridge refuses to start | Set a nonempty BRIDGE_TOKEN in the script. |
| Bridge receives no job | Ensure the server and script use the same MSSL_FORTH_BRIDGE_TOKEN, then use /1111 bridge-poll. |
| Variable text is malformed | Avoid special characters listed in the variable warning until escaping is corrected. |
| Object action is ignored | Confirm it came from Forth rather than RubyForth, and check normal Second Life permissions and parcel restrictions. |
Practical Workflow
- Use
/1111 forth ...for quick arithmetic or a short test. - Move multiline logic into a named inventory notecard.
- Upload with
/1111 notecard Nameafter every notecard edit. - Use
forth-runfor object actions andruby-runfor compact control-flow programs and server-side calculations. - Enable the bridge only when browser-queued work must reach a specific in-world object.