// Skimi Events Beacon v1.0.2 // grid.skimi3d.com - announce your event on Skimi Grid, from any grid. // // Rez this beacon where your event happens, touch it, follow the menus. // Your event is submitted for review; once approved it appears in the // in-world viewer search (Search > Events) on Skimi Grid and on // https://grid.skimi3d.com/events // // SECURITY BY DESIGN: this script holds NO secrets. It talks to a public // API; the beacon receives its own access token at runtime when it // registers, and every submission is reviewed by a human before it goes // live. Copying or opening this script reveals nothing you could abuse. // // The beacon also shows a countdown before your approved event and lights // up while it is running. Visitors can touch it to see what's on grid-wide. string API = "https://grid.skimi3d.com/api"; // ---- persistent state (survives script reset via object description) ---- string gToken; // runtime-issued access token string gGridUrl; // empty = beacon stands on Skimi Grid itself // ---- draft of the event being entered ---- string dName; string dDesc; integer dCat; string dRating; string dStart; integer dDur; // ---- runtime ---- integer gChan; integer gListen; string gStep; key reqRegister; key reqSubmit; key reqList; key reqHere; key gToucher; string gOwnerName; integer gPendingNew; string nextName; integer nextStart; integer nextDur; integer gLive; integer gTick; list CAT_LABELS = ["Discussion", "Sports", "Live Music", "Commercial", "Nightlife/Entertainment", "Games/Contests", "Pageants", "Education", "Arts and Culture", "Charity/Support Groups", "Miscellaneous"]; list CAT_IDS = [18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29]; saveState() { llSetObjectDesc("SEB1|" + gToken + "|" + gGridUrl); } loadState() { list p = llParseStringKeepNulls(llGetObjectDesc(), ["|"], []); if (llList2String(p, 0) == "SEB1") { gToken = llList2String(p, 1); gGridUrl = llList2String(p, 2); } } openListen() { llListenRemove(gListen); gListen = llListen(gChan, "", NULL_KEY, ""); } dialog(key who, string text, list buttons) { openListen(); llDialog(who, text, buttons, gChan); } textbox(key who, string text) { openListen(); llTextBox(who, text, gChan); } register() { string json = llList2Json(JSON_OBJECT, [ "owner_uuid", (string)llGetOwner(), "owner_name", gOwnerName, "region", llGetRegionName(), "grid_url", gGridUrl]); reqRegister = llHTTPRequest(API + "/beacon/register", [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json"], json); } submitEvent() { vector p = llGetPos(); string json = llList2Json(JSON_OBJECT, [ "token", gToken, "name", dName, "description", dDesc, "category", dCat, "rating", dRating, "start", dStart, "duration", dDur, "region", llGetRegionName(), "pos", (string)((integer)p.x) + "," + (string)((integer)p.y) + "," + (string)((integer)p.z), "grid_url", gGridUrl]); reqSubmit = llHTTPRequest(API + "/beacon/event", [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json"], json); } pollHere() { if (gToken == "") return; vector p = llGetPos(); reqHere = llHTTPRequest(API + "/beacon/here?token=" + llEscapeURL(gToken) + "®ion=" + llEscapeURL(llGetRegionName()) + "&pos=" + llEscapeURL((string)((integer)p.x) + "," + (string)((integer)p.y)), [HTTP_METHOD, "GET"], ""); } setIdle() { gLive = FALSE; llSetText("", <1,1,1>, 1.0); llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.0]); llParticleSystem([]); } showCountdown() { if (nextStart == 0) { setIdle(); return; } integer now = llGetUnixTime(); integer endT = nextStart + nextDur * 60; if (now > endT) { nextStart = 0; setIdle(); return; } if (now >= nextStart) { if (!gLive) { gLive = TRUE; llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, 0.25]); llParticleSystem([PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_INTERP_SCALE_MASK, PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE, PSYS_PART_START_COLOR, <0.6, 1.0, 0.9>, PSYS_PART_START_ALPHA, 0.6, PSYS_PART_START_SCALE, <0.08, 0.08, 0>, PSYS_PART_END_SCALE, <0.02, 0.02, 0>, PSYS_PART_MAX_AGE, 2.5, PSYS_SRC_BURST_RATE, 0.25, PSYS_SRC_BURST_PART_COUNT, 2, PSYS_SRC_BURST_RADIUS, 0.1, PSYS_SRC_BURST_SPEED_MIN, 0.2, PSYS_SRC_BURST_SPEED_MAX, 0.5, PSYS_SRC_ANGLE_BEGIN, 0.0, PSYS_SRC_ANGLE_END, 0.6]); } llSetText(nextName + "\nhappening NOW", <0.55, 1.0, 0.85>, 1.0); return; } if (gLive) setIdle(); integer mins = (nextStart - now) / 60; string t; if (mins >= 120) t = (string)(mins / 60) + " hours"; else if (mins >= 60) t = "1 hour " + (string)(mins % 60) + " min"; else t = (string)mins + " min"; llSetText(nextName + "\nstarts in " + t, <1.0, 0.95, 0.7>, 1.0); } mainMenu(key who) { if (who == llGetOwner()) dialog(who, "Skimi Events Beacon\n\nAnnounce an event at this spot, or see what's on across Skimi Grid.", ["New Event", "What's On", "Help"]); else { gToucher = who; reqList = llHTTPRequest(API + "/events/upcoming?format=txt&limit=8", [HTTP_METHOD, "GET"], ""); } } startFlow(key who) { if (gToken == "") { gPendingNew = TRUE; llOwnerSay("One moment, the beacon is registering itself..."); register(); return; } gStep = "name"; textbox(who, "Event name?\n(short and catchy, this is the line people see in search)"); } default { state_entry() { gChan = -100000 - (integer)llFrand(1000000.0); loadState(); llRequestAgentData(llGetOwner(), DATA_NAME); llSetTimerEvent(30.0); gTick = 18; // erste Standort-Abfrage kurz nach dem Start } on_rez(integer p) { llResetScript(); } changed(integer c) { if (c & (CHANGED_REGION | CHANGED_OWNER)) llResetScript(); } dataserver(key q, string data) { gOwnerName = data; if (gToken == "") register(); } touch_start(integer n) { mainMenu(llDetectedKey(0)); } listen(integer chan, string name, key who, string msg) { llListenRemove(gListen); if (who != llGetOwner()) return; if (gStep == "gridurl") { string a = llStringTrim(msg, STRING_TRIM); // Auf die Menue-Woerter darf diese Frage nicht reagieren, sonst // landet "New Event" als Grid-Adresse in der Datenbank. if (a == "Cancel" || a == "New Event" || a == "What's On" || a == "Help") { gStep = ""; llOwnerSay("No problem · touch me again when you have the address."); return; } // Eine Adresse hat einen Punkt und kein Leerzeichen. Alles andere ist // vermutlich versehentlich hier gelandet. if (llSubStringIndex(a, ".") == -1 || llSubStringIndex(a, " ") != -1) { llOwnerSay("\"" + a + "\" does not look like a grid address. It is the part your " + "viewer shows in the address bar, like yourgrid.example.com:8002 · touch me to try again."); gStep = ""; return; } gGridUrl = a; saveState(); register(); llOwnerSay("Thanks. Registering with your grid address..."); gStep = ""; return; } if (msg == "Cancel") { gStep = ""; llOwnerSay("Cancelled."); return; } if (msg == "Help") { llOwnerSay("Rez this beacon where your event happens and touch it to announce the event." + " A human reviews every submission; approved events show up in Search > Events on Skimi Grid" + " and on https://grid.skimi3d.com/events . The beacon shows a countdown and lights up" + " while your event runs. Visitors who touch it see what's on."); return; } if (msg == "New Event") { startFlow(who); return; } if (msg == "What's On") { gToucher = who; reqList = llHTTPRequest(API + "/events/upcoming?format=txt&limit=8", [HTTP_METHOD, "GET"], ""); return; } if (gStep == "name") { dName = llGetSubString(msg, 0, 199); gStep = "desc"; textbox(who, "Describe your event.\n(what happens, who is it for?)"); } else if (gStep == "desc") { dDesc = llGetSubString(msg, 0, 1999); gStep = "cat"; dialog(who, "Pick a category:", CAT_LABELS + ["Cancel"]); } else if (gStep == "cat") { integer i = llListFindList(CAT_LABELS, [msg]); if (i == -1) { gStep = ""; return; } dCat = llList2Integer(CAT_IDS, i); gStep = "rating"; dialog(who, "Content rating?", ["General", "Moderate", "Adult"]); } else if (gStep == "rating") { dRating = llGetSubString(msg, 0, 0); // G / M / A gStep = "start"; dialog(who, "When does it start?\n(times are grid time, PT)", ["In 1 hour", "In 2 hours", "In 3 hours", "Tonight 8 PM", "Tomorrow noon", "Tomorrow 8 PM", "Custom", "Cancel"]); } else if (gStep == "start") { if (msg == "In 1 hour") dStart = "rel:60"; else if (msg == "In 2 hours") dStart = "rel:120"; else if (msg == "In 3 hours") dStart = "rel:180"; else if (msg == "Tonight 8 PM") dStart = "today:20"; else if (msg == "Tomorrow noon") dStart = "tomorrow:12"; else if (msg == "Tomorrow 8 PM") dStart = "tomorrow:20"; else if (msg == "Custom") { gStep = "custom"; textbox(who, "Start date and time in grid time (PT), format:\nYYYY-MM-DD HH:MM\n(24h or with AM/PM, e.g. 2026-07-23 2:00 PM)"); return; } else { gStep = ""; return; } gStep = "dur"; dialog(who, "How long does it run?", ["30 min", "1 hour", "2 hours", "3 hours"]); } else if (gStep == "custom") { dStart = "abs:" + llStringTrim(msg, STRING_TRIM); gStep = "dur"; dialog(who, "How long does it run?", ["30 min", "1 hour", "2 hours", "3 hours"]); } else if (gStep == "dur") { if (msg == "30 min") dDur = 30; else if (msg == "1 hour") dDur = 60; else if (msg == "2 hours") dDur = 120; else if (msg == "3 hours") dDur = 180; else { gStep = ""; return; } gStep = ""; llOwnerSay("Submitting \"" + dName + "\" for review..."); submitEvent(); } } http_response(key req, integer code, list meta, string body) { if (req == reqRegister) { // Achtung LSL-Eigenheit: JSON-Booleans kommen als JSON_TRUE/JSON_FALSE-Konstanten, // nie als String "true". Erfolg deshalb am Token festmachen. string tok = llJsonGetValue(body, ["token"]); if (code == 200 && tok != JSON_INVALID && tok != "") { gToken = tok; saveState(); if (llJsonGetValue(body, ["need_grid"]) == JSON_TRUE) { gStep = "gridurl"; textbox(llGetOwner(), "This beacon is standing on a grid I do not know yet.\n\nWhat is THIS grid's address? It is what your viewer shows in the address bar, for example:\n yourgrid.example.com:8002\n\n(This is asked once. It is not the event name.)"); return; } // Sagen, dass es geklappt hat · sonst steht man da und raet. if (gGridUrl != "") { llOwnerSay("Registered. This beacon is linked to " + gGridUrl + " · events from here will point back to your grid. Touch me to announce one."); } else { llOwnerSay("Registered with Skimi Grid. Touch me to announce an event."); } showCountdown(); if (gPendingNew) { gPendingNew = FALSE; startFlow(llGetOwner()); } } else llOwnerSay("Could not reach Skimi Grid right now. Touch the beacon to try again later."); } else if (req == reqSubmit) { string m = llJsonGetValue(body, ["message"]); if (m == JSON_INVALID) m = ""; if (code == 200 && llJsonGetValue(body, ["id"]) != JSON_INVALID) llOwnerSay("Done. " + m); else if (m != "") llOwnerSay("Not accepted: " + m); else llOwnerSay("Something went wrong (" + (string)code + "). Please try again."); } else if (req == reqList) { if (code != 200 || llStringTrim(body, STRING_TRIM) == "") { llRegionSayTo(gToucher, 0, "Nothing on the grid calendar right now. Announce something at https://grid.skimi3d.com/events"); return; } llRegionSayTo(gToucher, 0, "What's on across Skimi Grid:"); list lines = llParseString2List(body, ["\n"], []); integer i; for (i = 0; i < llGetListLength(lines); i++) { list f = llParseStringKeepNulls(llList2String(lines, i), ["|"], []); llRegionSayTo(gToucher, 0, "* " + llList2String(f, 0) + " | " + llList2String(f, 1) + " | " + llList2String(f, 2) + " | " + llList2String(f, 3)); } } else if (req == reqHere) { if (code == 200 && body != "none" && llSubStringIndex(body, "|") != -1) { list f = llParseStringKeepNulls(body, ["|"], []); nextName = llList2String(f, 0); nextStart = (integer)llList2String(f, 1); nextDur = (integer)llList2String(f, 2); } else nextStart = 0; showCountdown(); } } timer() { // alle 30 s Anzeige lokal aktualisieren, alle 10 min den Server fragen gTick++; if (gTick >= 20) { gTick = 0; pollHere(); } else showCountdown(); } }