wolfCallAsync
Wolf Territoriesstring wolfCallAsync(string name, list args)
Like wolfCall, but the answer comes later as dataserver(query, data), where query is the key this returns. For plugin functions that take time (out-of-process services are always asynchronous). Returns NULL_KEY with wolfGetLastError saying why when refused (no dataserver event then). Defined on Wolf_Api.wolfCallAsync / IWolfPluginCalls.
Like wolfCall, but for plugin functions that take time: it returns a key straight away and the answer arrives later in your script's dataserver event, with that key as the query. The answer is one string; a function that answers at once sends its values joined with "|" (split them with llParseString2List(data, ["|"], [])). Functions provided by a separate service on the grid (rather than inside the region) always answer this way. If the plugin is not there, refuses, or its service is down, you get NULL_KEY at once and wolfGetLastError says why, starting "wolfCallAsync: " - then no dataserver event comes. If a service goes away while it is working on your call, the answer is a string starting with "error: ", so your script never waits for ever.
Parameters
| Type | Name | What it does |
|---|---|---|
| string | name | The plugin function to call, e.g. "weather.text". |
| list | args | What to give it: any list. |
Example
key asked;
default
{
touch_start(integer n)
{
asked = wolfCallAsync("weather.text", ["now"]);
if (asked == NULL_KEY)
llOwnerSay("Not asked: " + wolfGetLastError());
}
dataserver(key query, string data)
{
if (query == asked)
llSay(0, data); // the weather now in a sentence, or "error: ..." and why
}
}Wolf plugin commands on this grid
Every Wolf Territories region answers these names (the wolf-commands and wolf-hurricane plugins; the naval battle names, for The Caribbean only, are further down). With them a script anywhere on the region - rezzed on your land or worn as a HUD - runs your own WolfNPCs, a script in your own object makes it flash, and any script reads the region's weather. npc is the NPC's key (from npc.list, or its object's key); link is a link number as llSetLinkColor takes it (LINK_SET, LINK_THIS, LINK_ROOT, LINK_ALL_OTHERS, LINK_ALL_CHILDREN or a number).
| Name | Give it | You get back | Who may | What it does |
|---|---|---|---|---|
| wolf.version | [] | ["wolf-commands", version, 1] | anyone | Which version of these commands this region runs. |
| wolf.commands | [] | the command names | anyone | Every command in this list that this region answers. |
| npc.list | [] or ["all"] | [count, key, name, rights, ...] | anyone | Your NPCs on this region, or with "all" every NPC here. Rights is "edit" (yours to run), "control" (you may stop it, send it home, animate it, make a dragon breathe) or "none". At most 80 are listed; count is all of them. |
| npc.info | [npc] or [npc, "STATE" | "INTENT" | "EVENTS" | "CFG"] | [text] | control; CFG: owner | STATE is "intention|awake or asleep|alive or dead|health/max". INTENT is what it is doing, EVENTS its running events, CFG all its settings as JSON. |
| npc.say | [npc, text] | ["ok"] | owner | It says the words (up to 1000 characters) in local chat. An NPC speaks at most 5 lines in ten seconds. |
| npc.goto | [npc, <x, y, z>] or [npc, place] | ["ok"] | owner | It walks to that spot on this region, or to one of its own places (or tools) by name. |
| npc.follow | [npc, key or name] or [npc, ""] | ["ok"] | owner | It follows an avatar or NPC on this region; "" stops following. |
| npc.stop | [npc] | ["ok"] | control | It stops and stands. Its looped animations stop too. |
| npc.home | [npc] or [npc, "here"] | ["ok"] | control; "here": owner | It goes home. With "here", where it stands now becomes its home. |
| npc.anim | [npc, animation] or [npc, animation, "once" | "loop" | "stop"] | ["ok"] | control | Plays one of the animations in its contents once (the default), keeps it looping, or stops it. [npc, "", "stop"] stops them all. |
| npc.breath | [npc, "fire" | "ice" | "stop"] or [npc, "fire" | "ice", seconds] | ["ok"] | control | A dragon breathes fire or ice for 0.5 to 20 seconds (0 or none = 3), the same breath as in its window. |
| object.flash | [link, colour A, colour B] or [link, A, B, seconds each, seconds in all, face] | ["ok", prims, seconds each, seconds in all] | your own object | Flashes prims of the script's own object between two colours, and puts their own colours back when it ends. Each colour shows 0.5 to 60 seconds (default 1); it lasts up to 3600 seconds (default 10; 0 = until stopped, at most an hour); face is ALL_SIDES (the default) or a face number. See "Flashing an object" below. |
| object.flash.stop | [] or [link] | ["ok", prims stopped] | your own object | Stops the flash on those prims ([] = the whole object) and puts their own colours back. |
| weather.now | [] or ["json"] | [name, value, name, value, ...] or [JSON] | anyone | The weather on this region now: the condition, temperature, feels-like, humidity, pressure, wind, cloud, rain or snow, thunder, the sun and moon, the place and its local time. See "Weather" below for every name and unit. |
| weather.forecast | [], [hours] or [hours, "json"] | [count, 8, unix, "HH:MM", °C, condition, mm, cloud %, wind m/s, wind from °, ...] or [JSON] | anyone | The next hours, one row an hour (24 by default; the list holds up to 31 hours, JSON up to 48). |
| weather.text | [], ["now"], ["stats"], ["later"] or [hours ahead] | [text] | anyone | Ready to say: a sentence about now ("It's 8°C in Tromsø with light rain and a fresh westerly wind. Sunset is at 15:42."), the full stats block, or the weather later ("later" = about 6 hours ahead; or 1-48 hours). |
| weather.set | [weather] or [weather, minutes] | "ok|weather|minutes" (wolfCallAsync) | region holders | Sets the region's weather: clear, fair, cloudy, overcast, rain, storm, snow or fog, or a pattern (settled, changeable, unsettled, stormy, wintry). With minutes (1-1440) it goes back by itself afterwards; with 0 or no minutes it stays until changed. wolfCallAsync only. |
| weather.auto | [] | "ok|live|0" (wolfCallAsync) | region holders | Back to the real place's live weather. wolfCallAsync only. |
| weather.hurricane | [] or [name, value, ...]: "category", "minutes", "form", "centre", "radius", "heading", "speed", "rain" | "ok|id|category|form s|mature s|decay s|radius|eye|hemisphere" (wolfCallAsync) | region holders | A hurricane forms from the region's own clouds, rages, then dies away back into the region's own weather. Everyone on the region sees it. One at a time. wolfCallAsync only. |
| weather.hurricane.end | [] or ["now"] | "ok|decaying|seconds left" or "ok|gone" (wolfCallAsync) | region holders | It dies away now: after at least a minute at full strength (one still forming finishes forming first), over at most 5 minutes. With "now" it is gone at once. |
| weather.hurricane.state | [] | ["none"] or [stage, category, seconds left, <eye x, y, 0>, radius, eye radius, wind m/s, organisation, eye clear, hemisphere, id] | anyone | The storm now: "forming", "mature" or "decaying", and where its eye is (region metres). |
Who may
- owner: the NPC's owner, while they may still build where it lives - they own the region or are one of its estate managers, or they own the parcel under its home (a travelling pet: anywhere). The same rule as editing it in the NPC window.
- control: the owner as above, or the region's owner or an estate manager - what the NPC window lets them do with anyone's NPC. They can never make someone else's NPC speak, walk, follow, move its home or read its settings.
- your own object: object.flash only ever changes the object the script is in - it takes no key - and, like every command that acts for someone, only when the script belongs to that object's owner.
- anyone for the weather: any script may read it, because it is what everyone on the region sees. region holders: weather.set, weather.auto and the hurricane only work for the region's owner, its estate owner, an estate manager or the grid's staff (the grid checks, as the Wolf Environment window does).
- Every command that acts for someone (npc.list and the other npc commands, object.flash, weather.set, weather.auto, weather.hurricane and weather.hurricane.end) needs the script to belong to the owner of the object it is in: someone else's script dropped into your object never acts for you. The ones that only tell you something (wolf.version, wolf.commands, weather.now, weather.forecast, weather.text and weather.hurricane.state) work from any script. Only NPCs on the same region.
Answers and limits
- Every command answers at least one value. An empty list (wolfCall) or NULL_KEY (wolfCallAsync) means it was refused, and wolfGetLastError() says why in plain words, starting "wolfCall: " or "wolfCallAsync: " - for example "wolfCall: only its owner can do that".
- ["ok"] means the NPC has been told; it does it straight away, even while it is asleep with nobody near.
- With wolfCallAsync the answer comes in dataserver as one string, the values joined with "|".
- At most 20 calls in ten seconds from one object, 60 from all of one resident's objects, and 600 from the whole region; an NPC says at most 5 lines in ten seconds. Over that, the call is refused with a message and works again a few seconds later.
Example: make your NPC come to you and say hello
// Touch: your first NPC on this region walks to you and greets you.
touch_start(integer n)
{
if (llDetectedKey(0) != llGetOwner()) return;
list mine = wolfCall("npc.list", []); // [count, key, name, rights, ...]
if (llList2Integer(mine, 0) == 0)
{
llOwnerSay("No NPCs of yours here. " + wolfGetLastError());
return;
}
key npc = llList2Key(mine, 1);
if (wolfCall("npc.goto", [npc, llDetectedPos(0)]) == [])
llOwnerSay("It will not come: " + wolfGetLastError());
else
wolfCall("npc.say", [npc, "Hello, " + llDetectedName(0) + "!"]);
}A whole NPC remote to wear as a HUD (wolf_npc_remote.lsl, 113 lines: show or hide)
// Wolf NPC remote - wear it as a HUD (or rez it on your land) and control your own WolfNPCs on this region.
// Uses the wolf-commands plugin through wolfCall: no region restart is ever needed for these commands.
// Touch: pick one of your NPCs, then what it should do. Only NPCs you own (and may still run here) obey you.
// Wolf Territories, 2026. Copy, change and share freely.
integer CHANNEL;
integer listener;
list npcKeys;
list npcNames;
key chosen;
string chosenName;
// Every wolf-commands call answers with a list; an empty list means "refused", and wolfGetLastError() says why.
integer tell(string what, list answer)
{
if (llGetListLength(answer) == 0)
{
llOwnerSay(what + ": " + wolfGetLastError());
return FALSE;
}
return TRUE;
}
listenFor(float seconds)
{
llListenRemove(listener);
listener = llListen(CHANNEL, "", llGetOwner(), "");
llSetTimerEvent(seconds);
}
pickNpc()
{
// [count, key, name, rights, key, name, rights, ...] - rights is "edit", "control" or "none".
list r = wolfCall("npc.list", []);
if (!tell("Your NPCs", r)) return;
npcKeys = [];
npcNames = [];
integer i;
for (i = 1; i + 2 < llGetListLength(r) && llGetListLength(npcNames) < 12; i += 3)
{
if (llList2String(r, i + 2) != "none")
{
npcKeys += llList2Key(r, i);
npcNames += llGetSubString(llList2String(r, i + 1), 0, 23);
}
}
if (npcNames == [])
{
llOwnerSay("You have no NPCs you can control on this region.");
return;
}
listenFor(60.0);
llDialog(llGetOwner(), "Which NPC?", npcNames, CHANNEL);
}
pickAction()
{
listenFor(60.0);
llDialog(llGetOwner(), chosenName + " - what should it do?",
["Come here", "Follow me", "Stop", "Go home", "Say hello", "Status", "Fire", "Ice"], CHANNEL);
}
default
{
state_entry()
{
CHANNEL = -1 - (integer)("0x" + llGetSubString((string)llGetKey(), 0, 6));
chosen = NULL_KEY;
}
touch_start(integer n)
{
if (llDetectedKey(0) == llGetOwner()) pickNpc();
}
listen(integer ch, string name, key id, string msg)
{
integer i = llListFindList(npcNames, [msg]);
if (chosen == NULL_KEY || i != -1)
{
if (i == -1) return;
chosen = llList2Key(npcKeys, i);
chosenName = msg;
pickAction();
return;
}
list r;
if (msg == "Come here") r = wolfCall("npc.goto", [chosen, llList2Vector(llGetObjectDetails(llGetOwner(), [OBJECT_POS]), 0)]);
else if (msg == "Follow me") r = wolfCall("npc.follow", [chosen, llGetOwner()]);
else if (msg == "Stop") r = wolfCall("npc.stop", [chosen]);
else if (msg == "Go home") r = wolfCall("npc.home", [chosen]);
else if (msg == "Say hello") r = wolfCall("npc.say", [chosen, "Hello, " + llKey2Name(llGetOwner()) + "!"]);
else if (msg == "Fire" || msg == "Ice") r = wolfCall("npc.breath", [chosen, llToLower(msg), 3.0]); // dragons only
else if (msg == "Status")
{
// "intention|awake or asleep|alive or dead|health/max"
r = wolfCall("npc.info", [chosen]);
if (tell(chosenName, r)) llOwnerSay(chosenName + ": " + llList2String(r, 0));
}
if (msg != "Status") tell(chosenName, r);
chosen = NULL_KEY;
llListenRemove(listener);
llSetTimerEvent(0.0);
}
timer()
{
llListenRemove(listener);
llSetTimerEvent(0.0);
chosen = NULL_KEY;
}
}
Flashing an object
wolfCall("object.flash", [link, colour A, colour B, seconds each, seconds in all, face]) makes prims of your object flash between two colours. The region does the flashing, so your script needs no timer, and when the flash ends - or you stop it - every face gets its own colour back. Only the first three values are needed.
- Colours are vectors from <0, 0, 0> to <1, 1, 1>, as llSetColor takes them (values outside are cut to that range); the two must differ. The first colour shows at once. Only the colour changes: transparency, textures, glow and everything else stay as they are.
- Seconds each colour: 0.5 to 60, default 1, rounded to the nearest quarter of a second. Seconds in all: up to 3600, default 10; 0 means until stopped, at most an hour - call it again to go on.
- Face: ALL_SIDES (the default) or a face number; prims of the link set without that face are left out.
- Calling it again on prims that are flashing starts a new flash on them, and their own colours are still the ones put back.
When it stops by itself
- The time is up, or object.flash.stop is called: the colours go back.
- The script that started it is removed, set to not running, or reset with Reset Scripts: the colours go back. A reset from inside the script (llResetScript) or saving the script again in the editor is not seen - stop the flash first, or start it again from state_entry.
- A prim is unlinked from the object: that prim gets its colours back. If the prim holding the script is unlinked, the whole flash ends and the colours go back.
- You (or another script) change a flashing prim's colour: your colour wins and that prim stops flashing.
- The object is deleted or returned, or the region restarts: the flash ends. When the region saves the object while it flashes, it saves the object's own colours.
Limits
- Rezzed objects only, not attachments or HUDs: a worn object is saved to your inventory when you take it off, teleport or log out, and it would keep the flash colour.
- If you Take, Take Copy, duplicate or return an object while it flashes, or save the region as an OAR, that copy keeps the colour it showed at that moment. Stop the flash first.
- At most 16 prims of one object, 32 of yours and 64 on the whole region flash at once. Every colour change is sent to every viewer that can see the prim, so these limits keep a busy region smooth for everyone.
- The flash itself does not trigger changed(CHANGED_COLOR), so scripts in the prim are not woken twice a second.
- When nobody is on or next to the region, nothing is sent; the flash picks up when someone arrives.
- The calls count towards the limits above (20 calls in ten seconds from one object).
Example: flash red and blue for five seconds when touched
touch_start(integer n)
{
// [link, colour A, colour B, seconds each colour, seconds in all, face]
list r = wolfCall("object.flash", [LINK_SET, <1, 0, 0>, <0, 0, 1>, 0.5, 5.0, ALL_SIDES]);
if (r == [])
llOwnerSay("Could not flash: " + wolfGetLastError());
}Touch to flash, touch again to stop (wolf_flash.lsl, 43 lines: show or hide)
// Wolf Flash - touch to make this object flash between two colours; touch again to stop.
// Uses the wolf-commands plugin through wolfCall("object.flash", ...): the region does the flashing, so this script
// needs no timer, and the object's own colours come back when the flash ends or is stopped.
// Works on rezzed objects you own (not on attachments or HUDs). Wolf Territories, 2026. Copy, change and share freely.
vector COLOUR_A = <1.0, 0.0, 0.0>; // red
vector COLOUR_B = <1.0, 1.0, 0.0>; // yellow
float EACH = 0.5; // seconds each colour shows: 0.5 to 60
float FOR = 0.0; // seconds the flash lasts: 0 = until stopped (at most an hour)
integer flashing;
default
{
touch_start(integer n)
{
if (llDetectedKey(0) != llGetOwner()) return;
if (!flashing)
{
// [link, colour A, colour B, seconds each colour, seconds in all, face]
list r = wolfCall("object.flash", [LINK_SET, COLOUR_A, COLOUR_B, EACH, FOR, ALL_SIDES]);
if (llGetListLength(r) == 0)
{
llOwnerSay("Could not flash: " + wolfGetLastError());
return;
}
// ["ok", prims flashing, seconds each colour, seconds in all]
llOwnerSay("Flashing " + llList2String(r, 1) + " prims. Touch again to stop.");
flashing = TRUE;
}
else
{
// ["ok", prims stopped]: their own colours are back
wolfCall("object.flash.stop", [LINK_SET]);
flashing = FALSE;
}
}
on_rez(integer p)
{
llResetScript();
}
}
Weather
Your scripts can read the weather the region shows - the real weather of its place, or the weather its owner chose - and say it, show it or act on it. It is the same report the Region API gives. Real weather comes from MET Norway; the region reads it at most every few minutes, so ask as often as you like but expect a new answer about every 5 minutes.
- Use wolfCallAsync the first time. The region reads its weather when a script first asks; wolfCall answers straight away only once it has it (until then it is refused with "the weather is being read just now"). wolfCallAsync waits and answers in dataserver.
- Units: temperatures in °C (and °F in the names ending _f), wind in m/s (wind_kmh in km/h), wind direction in degrees the wind comes from (0 = north, 90 = east) with a 16-point compass name, pressure in hPa at sea level, humidity, cloud, fog and chances in %, rain or snow in mm in the hour, sun and moon heights in degrees above the horizon. Times are ISO 8601 (UTC with Z, local with its offset).
- Not known is left out. A value the weather does not have (no temperature for weather chosen by the owner, no gusts outside some areas, no visibility at all) is not in the list - check with llListFindList. In the forecast list an unknown value is "".
- Where it comes from: source_line says it; show it with the weather. Real weather is MET Norway's (CC BY 4.0) and must be credited: "Source: MET Norway (met.no)". The thunder risk outside the Nordic countries is from Open-Meteo.com.
- It is the region's weather. A parcel with its own weather (About Land, a parcel Wolf Environment, a wolfrain prim) is not included.
weather.now: the names
| Names | What (type, unit) |
|---|---|
| available, reason | integer 1 or 0; when 0, reason says why (string) |
| region, place, place_short, country, lat, lon | the region; the place its weather is for, its first part, its country (strings); latitude, longitude (float, degrees) |
| timezone, utc_offset, utc_offset_text, local_time, local_time24, local_time12, grid_time24 | the place's time zone (e.g. Europe/Oslo), its offset now (integer seconds; "UTC+1"), its time now (ISO; "08:24"; "08:24 AM"), the grid's time ("00:24") |
| control_by, control_mode, control_source, control_type, control_pattern, control_place_kind | what runs the weather: wolf-environment, auto-environment, region-weather or none; source live (the real place), fixed, pattern, manual (set by hand) or none; the type or pattern chosen |
| condition, symbol, weather_code | e.g. "Few clouds"; MET's symbol (e.g. "fair_day"); the weather id (integer, OpenWeather-style: 800 clear ... 804 overcast) |
| kind, level, precip_type, precipitation_mm, precipitation_chance | what falls on the region: clear, rain or snow, level 0-4; rain, snow, sleet or none; mm in the hour (float); chance in % (integer) |
| temperature_c, temperature_f, feels_like_c, feels_like_f, dew_point_c | floats. Feels-like is the wind chill at or below 10 °C with wind, the heat index when hot and humid, else the air temperature |
| humidity, pressure_hpa | integer %; float hPa at sea level |
| wind_ms, wind_kmh, wind_from_deg, wind_compass, wind_beaufort, wind_words, gust_ms | floats m/s and km/h; integer degrees (where it comes from); "NW"; Beaufort force 0-12 (integer) and its words ("fresh breeze"); gusts m/s |
| cloud, cloud_low, cloud_medium, cloud_high, fog, thunder_chance, uv_index, aurora | integers in % (uv_index a float); aurora = the chance of seeing one, % |
| is_day, sun_elevation, sun_azimuth, sunrise, sunset, sunrise_local, sunset_local, polar, next_sun_event, next_sun_event_time, next_sun_event_local | integer 1 or 0; floats in degrees; ISO times and "HH:MM" at the place; polar = "midnight sun" or "polar night" when the sun does not rise or set; the next sunrise or sunset |
| moon_elevation, moon_azimuth, moon_illuminated, moon_waxing, moon_phase | floats in degrees; integer % lit; 1 or 0; e.g. "waxing gibbous" |
| reading_time, fetched_at, stale, synthetic, age_s, source_line | the hour MET's reading is for and when it was fetched (ISO); 1 when it is over 6 hours old; 1 for weather chosen by the owner; seconds since the region read it; the credit to show |
["json"] gives the same as one JSON string, grouped (now, place, sky, control, text, attribution, units) - read it with llJsonGetValue, e.g. llJsonGetValue(j, ["now", "temperature_c"]).
Changing the weather (region holders)
- wolfCallAsync("weather.set", ["rain", 30]) makes it rain on the whole region for 30 minutes, then puts back the weather it had - unless someone changed the region's environment in the meantime, then theirs stays. Without minutes it stays until changed. Every viewer on the region sees it change, as when the Wolf Environment is saved.
- It changes the region's Wolf Environment weather. A region that still runs Jimmy Olsen's Automatic Environment gets a Wolf Environment with the same place and clock (and loses it again when a timed change ends). A region whose sky is its own EEP, whose weather is set by hand in Region/Estate > Weather, or whose Wolf Environment is switched off, is refused with what to do.
- weather.auto goes back to the real place's weather (a region with a made-up place has none, and is refused).
- The answer comes in dataserver: ok|rain|30, or error: and the reason (for example "only the region's owner, its estate owner or an estate manager can change the region's weather").
- At most 3 changes a minute from one resident and 6 on the region.
Example: say the weather when touched
key ask;
default
{
touch_start(integer n)
{
// "now" (the default), "stats", "later" or a number of hours ahead
ask = wolfCallAsync("weather.text", ["now"]);
if (ask == NULL_KEY) llSay(0, wolfGetLastError());
}
dataserver(key q, string data)
{
if (q != ask) return;
if (llGetSubString(data, 0, 5) == "error:") data = llGetSubString(data, 7, -1);
llSay(0, data); // It's 8°C in Tromsø with light rain and a fresh westerly wind. Sunset is at 15:42.
}
}Example: read single values (once the region has its weather)
list w = wolfCall("weather.now", []);
integer i = llListFindList(w, ["temperature_c"]);
if (i != -1)
llOwnerSay("It is " + (string)llRound(llList2Float(w, i + 1)) + " °C here. "
+ llList2String(w, llListFindList(w, ["source_line"]) + 1));What the words look like - the region TROMSO on 26 October 2026 at 08:57 there:
weather.text ["now"]: It's 9°C in Tromsø with scattered cloud and a gentle southerly wind. Sunset is at 15:16. weather.text ["later"]: Later this afternoon (15:00): overcast skies. weather.text ["stats"]: 🌍 Weather for Tromsø, Norge: Scattered clouds 🌡 9.30 °C (48.70 °F), feels like 7 °C 💧 Humidity: 68% ⏲ Pressure: 994 hPa 🌬 Wind: 4.90 m/s @ 158° (SSE) 🌅 Sunrise 07:39, sunset 15:16 🕒 Local time: 08:57 (24h) / 08:57 AM (AM/PM) [UTC+1] Source: MET Norway (met.no)
A weather board: the weather as floating text, rain or snow on it when it falls here (wolf_weather_board.lsl, 116 lines: show or hide)
Download wolf_weather_board.lsl
// Wolf weather board - shows this region's weather as floating text and makes it rain or snow on the board when it
// does here. Put it in a prim; it works on any Wolf Territories region. (wolf-commands 1.2.0: wolfCall weather.*)
//
// It asks every 5 minutes: the region keeps the weather for 5 minutes anyway, so asking more often only gives the
// same answer. Touch it to see the full stats in local chat.
float REFRESH = 300.0; // seconds between looks
key gAsk; // the weather.now question waiting for its answer
key gStats; // the weather.text "stats" question
string num(string json, list path, integer places)
{
string v = llJsonGetValue(json, path);
if (v == JSON_INVALID || v == JSON_NULL) return "?";
float f = (float)v;
if (places == 0) return (string)llRound(f);
// one decimal place
integer tenths = llRound(f * 10.0);
string s = (string)(tenths / 10) + "." + (string)llAbs(tenths % 10);
if (tenths < 0 && tenths > -10) s = "-" + s;
return s;
}
falling(string kind)
{
if (kind == "rain" || kind == "sleet")
{
llParticleSystem([
PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK,
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE,
PSYS_SRC_ANGLE_BEGIN, 0.0, PSYS_SRC_ANGLE_END, PI,
PSYS_PART_START_COLOR, <0.7, 0.8, 1.0>, PSYS_PART_START_ALPHA, 0.6,
PSYS_PART_START_SCALE, <0.03, 0.25, 0.0>,
PSYS_SRC_ACCEL, <0.0, 0.0, -9.0>,
PSYS_SRC_BURST_RATE, 0.05, PSYS_SRC_BURST_PART_COUNT, 6,
PSYS_SRC_BURST_RADIUS, 1.5, PSYS_PART_MAX_AGE, 1.2,
PSYS_SRC_BURST_SPEED_MIN, 0.1, PSYS_SRC_BURST_SPEED_MAX, 0.3]);
}
else if (kind == "snow")
{
llParticleSystem([
PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_WIND_MASK,
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE,
PSYS_SRC_ANGLE_BEGIN, 0.0, PSYS_SRC_ANGLE_END, PI,
PSYS_PART_START_COLOR, <1.0, 1.0, 1.0>, PSYS_PART_START_ALPHA, 0.9,
PSYS_PART_START_SCALE, <0.08, 0.08, 0.0>,
PSYS_SRC_ACCEL, <0.0, 0.0, -0.6>,
PSYS_SRC_BURST_RATE, 0.1, PSYS_SRC_BURST_PART_COUNT, 3,
PSYS_SRC_BURST_RADIUS, 1.5, PSYS_PART_MAX_AGE, 5.0,
PSYS_SRC_BURST_SPEED_MIN, 0.05, PSYS_SRC_BURST_SPEED_MAX, 0.2]);
}
else llParticleSystem([]);
}
ask()
{
// wolfCallAsync answers in dataserver when the weather is ready (the very first look on a region can take a second).
gAsk = wolfCallAsync("weather.now", ["json"]);
if (gAsk == NULL_KEY) llSetText("Weather: " + wolfGetLastError(), <1.0, 0.6, 0.6>, 1.0);
}
default
{
state_entry()
{
llSetText("Reading the weather...", <1.0, 1.0, 1.0>, 1.0);
ask();
llSetTimerEvent(REFRESH);
}
on_rez(integer p) { llResetScript(); }
timer() { ask(); }
touch_start(integer n)
{
gStats = wolfCallAsync("weather.text", ["stats"]);
if (gStats == NULL_KEY) llRegionSayTo(llDetectedKey(0), 0, wolfGetLastError());
}
dataserver(key q, string data)
{
if (llGetSubString(data, 0, 5) == "error:")
{
if (q == gAsk) llSetText("Weather: " + llGetSubString(data, 7, -1), <1.0, 0.6, 0.6>, 1.0);
else if (q == gStats) llSay(0, llGetSubString(data, 7, -1));
return;
}
if (q == gStats) { llSay(0, data); return; }
if (q != gAsk) return;
if (llJsonGetValue(data, ["available"]) != JSON_TRUE)
{
llSetText("No weather here\n" + llJsonGetValue(data, ["reason"]), <0.8, 0.8, 0.8>, 1.0);
falling("none");
return;
}
string place = llJsonGetValue(data, ["place", "short"]);
string text = llJsonGetValue(data, ["now", "condition"]) + "\n";
if (llJsonGetValue(data, ["now", "temperature_c"]) != JSON_NULL)
text += num(data, ["now", "temperature_c"], 1) + " °C (feels like " + num(data, ["now", "feels_like_c"], 0) + " °C)\n";
if (llJsonGetValue(data, ["now", "humidity"]) != JSON_NULL)
text += "Humidity " + num(data, ["now", "humidity"], 0) + "%\n";
text += "Wind " + num(data, ["now", "wind_ms"], 1) + " m/s from the " + llJsonGetValue(data, ["now", "wind_compass"]) + "\n";
if (llJsonGetValue(data, ["place", "local_time24"]) != JSON_INVALID)
text += place + " time " + llJsonGetValue(data, ["place", "local_time24"]) + "\n";
text += llJsonGetValue(data, ["attribution", "text"]);
llSetText(text, <1.0, 1.0, 1.0>, 1.0);
// Rain, sleet or snow on the board while it falls here.
string kind = llJsonGetValue(data, ["now", "precip_type"]);
if (llJsonGetValue(data, ["now", "kind"]) == "clear") kind = "none";
falling(kind);
}
}
Ask it in local chat: "what's the weather?", "will it rain later?" (owner touch: on/off) (wolf_weather_ask.lsl, 96 lines: show or hide)
// Wolf weather - ask it in local chat. Say "what is the weather now", "what's the weather", "will it rain later" or
// "weather forecast" near it and it answers you. Touch it (owner only) to switch it on or off.
// Works on any Wolf Territories region. (wolf-commands 1.2.0: wolfCall weather.text)
//
// It answers only the person who asked (llRegionSayTo), at most once every 20 seconds each and once every 5 seconds in
// all, so it cannot be used to flood the chat.
integer PERSON_WAIT = 20; // seconds before the same person gets another answer
integer ALL_WAIT = 5; // seconds between any two answers
integer MAX_REMEMBERED = 40; // people remembered for PERSON_WAIT (oldest forgotten first)
integer gOn = TRUE;
integer gListen;
integer gLastAnswer;
list gAsked; // [avatar, unix time, ...]
list gWaiting; // [query key, avatar, ...] questions waiting for their answer
// What they asked for: "now", "later", "stats" - or "" when it is not a weather question.
string kindOf(string msg)
{
string m = llToLower(msg);
if (llSubStringIndex(m, "weather") == -1 && llSubStringIndex(m, "rain") == -1 && llSubStringIndex(m, "snow") == -1
&& llSubStringIndex(m, "forecast") == -1 && llSubStringIndex(m, "temperature") == -1) return "";
// a question or a request, not every sentence that mentions rain
if (llSubStringIndex(m, "?") == -1 && llSubStringIndex(m, "what") == -1 && llSubStringIndex(m, "will it") == -1
&& llSubStringIndex(m, "forecast") == -1 && llSubStringIndex(m, "how") == -1 && llSubStringIndex(m, "is it") == -1) return "";
if (llSubStringIndex(m, "stats") != -1 || llSubStringIndex(m, "detail") != -1 || llSubStringIndex(m, "report") != -1) return "stats";
if (llSubStringIndex(m, "later") != -1 || llSubStringIndex(m, "forecast") != -1 || llSubStringIndex(m, "tonight") != -1
|| llSubStringIndex(m, "tomorrow") != -1 || llSubStringIndex(m, "will it") != -1) return "later";
return "now";
}
integer mayAnswer(key who)
{
integer now = llGetUnixTime();
if (now - gLastAnswer < ALL_WAIT) return FALSE;
integer i = llListFindList(gAsked, [who]);
if (i != -1)
{
if (now - llList2Integer(gAsked, i + 1) < PERSON_WAIT) return FALSE;
gAsked = llDeleteSubList(gAsked, i, i + 1);
}
gAsked += [who, now];
if (llGetListLength(gAsked) > MAX_REMEMBERED * 2) gAsked = llDeleteSubList(gAsked, 0, 1);
gLastAnswer = now;
return TRUE;
}
show()
{
if (gOn) llSetText("Ask me about the weather", <0.7, 0.9, 1.0>, 1.0);
else llSetText("Weather (off)", <0.6, 0.6, 0.6>, 1.0);
}
default
{
state_entry()
{
gListen = llListen(0, "", NULL_KEY, "");
show();
}
on_rez(integer p) { llResetScript(); }
touch_start(integer n)
{
if (llDetectedKey(0) != llGetOwner()) return;
gOn = !gOn;
llListenControl(gListen, gOn);
show();
llOwnerSay("Weather answers " + llList2String(["off", "on"], gOn) + ".");
}
listen(integer ch, string name, key id, string msg)
{
if (!gOn || llGetAgentSize(id) == ZERO_VECTOR) return; // people only, not objects
string kind = kindOf(msg);
if (kind == "" || !mayAnswer(id)) return;
// "later" is about six hours ahead; weather.text also takes a number of hours (1-48).
key q = wolfCallAsync("weather.text", [kind]);
if (q == NULL_KEY) { llRegionSayTo(id, 0, wolfGetLastError()); return; }
gWaiting += [q, id];
if (llGetListLength(gWaiting) > 20) gWaiting = llDeleteSubList(gWaiting, 0, 1);
}
dataserver(key q, string data)
{
integer i = llListFindList(gWaiting, [q]);
if (i == -1) return;
key who = llList2Key(gWaiting, i + 1);
gWaiting = llDeleteSubList(gWaiting, i, i + 1);
if (llGetSubString(data, 0, 5) == "error:") data = "Sorry, I can't tell just now: " + llGetSubString(data, 7, -1);
llRegionSayTo(who, 0, data);
}
}
Hurricanes
A script can bring a hurricane over the region. It forms from the clouds already in the sky: they gather, thicken and start to turn, spiral rain bands wind in, a dense overcast builds over the middle, then the eye clears inside its eyewall. It rages for a while, crossing the region, then dies away and the region's own weather comes back exactly as it was. It turns anticlockwise in the northern hemisphere and clockwise in the southern, by the latitude of the region's weather place.
- Everything follows it: the region's wind (llWind, and the wind every viewer gets: particles, flexi prims, trees, boats) circulates round the eye at the category's speed; heavy rain and thunder in the eyewall and bands, none in the eye; the sky darkens; the sea gets up where the Wolf Environment's water follows the wind. WolfViewer and WolfStorm draw the storm itself (the bands, the eye, the eyewall); other viewers see the darker, stormy sky, the rain and the wind. Clouds never cover anyone's builds.
- Options (all optional, name then value): "category" a whole number 1-5 (3, not 3.0; Saffir-Simpson, default 1); "minutes" 5-300 in all (default 40); "form" 2-60 minutes forming (default 10; it dies away over a quarter of its time, 2-30 minutes); "centre" <x, y, 0> where the eye passes, in region metres (default the middle); "radius" 1000-60000 metres (default: a standard storm about 1500 metres across from its eye to its edge, a little bigger or smaller each time, 1125-1875, the same on every region so you see the whole storm come in, pass and go); "heading" 0-360, the way it moves (default downwind, else west); "speed" 0-30 m/s (default: it crosses twice its own radius in its lifetime, so it arrives, passes over the centre half way through and leaves; 0 = it stays); "rain" TRUE or FALSE (a whole number: 1 or 0, not 1.0). "center" works as well as "centre", and the names are not case-sensitive.
- Sizes and speeds: a real hurricane's shape shrunk to a size you can see from the region (the eye 15 % of the storm's radius, about 170-280 metres for the standard storm), with real winds (category 1 about 38 m/s, category 5 about 70 m/s at the eyewall, from the NHC's Saffir-Simpson scale) and real rain rates.
- Who: the region's owner, its estate owner, an estate manager or the grid's staff (the grid checks, as for weather.set); the script must belong to its object's owner. Anyone's script can read weather.hurricane.state, at most 20 times in ten seconds from one object. At most 3 starts or ends a minute from one resident and 6 on the region.
- Where: as weather.set, a region whose sky is run by its Wolf Environment or by the automatic environment. A region whose sky is its own EEP, whose weather is set by hand, or whose Wolf Environment is switched off, is refused with what to do. One hurricane per region: a second one is refused until the first has ended.
- The answer comes in dataserver: ok|..., or error: and the reason. Saving the Wolf Environment window while a storm runs keeps it.
Example: a category 3 hurricane, 60 minutes, crossing the region from the east
key ask;
default
{
touch_start(integer n)
{
if (llDetectedKey(0) != llGetOwner()) return;
ask = wolfCallAsync("weather.hurricane", ["category", 3, "minutes", 60, "form", 15, "heading", 270]);
if (ask == NULL_KEY) llOwnerSay(wolfGetLastError());
}
dataserver(key q, string data)
{
if (q == ask) llOwnerSay(data); // ok|5d0c...|3|900|1800|900|4000|400|1 or error: ...
}
}A hurricane button: a menu to start one (category 1-5), let it die away or end it, and its state as floating text (wolf_hurricane.lsl, 84 lines: show or hide)
// Wolf Territories - a hurricane button (wolf-hurricane, wolfCall weather.hurricane*).
// Put it in a prim on your region. Touch it (its owner only) for a menu: start a hurricane of category 1 to 5, let it die
// away now, or take it away at once. While a storm runs the prim shows its stage, the time left and the wind as floating
// text. Only the region's owner, its estate owner or an estate manager can make a hurricane (the grid checks); anyone's
// script can read its state.
integer CHANNEL;
integer listener;
key ask;
string asked;
show()
{
list s = wolfCall("weather.hurricane.state", []);
if (llGetListLength(s) == 0 || llList2String(s, 0) == "none")
{
llSetText("No hurricane\n(touch for the menu)", <0.8, 0.9, 1.0>, 1.0);
llSetTimerEvent(0.0); // nothing to watch: no timer while no storm runs
return;
}
// [stage, category, seconds left, <eye x, y, 0>, radius, eye radius, wind now, organisation, eye clear, hemisphere, id]
string stage = llList2String(s, 0);
if (stage == "decaying") stage = "dying away";
integer left = llList2Integer(s, 2);
llSetText("Category " + llList2String(s, 1) + " hurricane: " + stage
+ "\n" + (string)(left / 60) + " min " + (string)(left % 60) + " s left"
+ "\nstrongest wind now " + (string)llRound(llList2Float(s, 6)) + " m/s"
+ "\nwind here " + (string)llRound(llVecMag(llWind(ZERO_VECTOR))) + " m/s", <1.0, 0.8, 0.4>, 1.0);
}
default
{
state_entry()
{
CHANNEL = -1 - (integer)("0x" + llGetSubString((string)llGetKey(), 0, 6));
llSetTimerEvent(5.0);
show();
}
on_rez(integer p) { llResetScript(); }
touch_start(integer n)
{
if (llDetectedKey(0) != llGetOwner()) return;
llListenRemove(listener);
listener = llListen(CHANNEL, "", llGetOwner(), "");
llDialog(llGetOwner(), "Hurricane over this region\n\nStart one (it forms from the clouds over 10 minutes and lasts 40), let the one now die away, or take it away at once.",
["Cat 1", "Cat 2", "Cat 3", "Cat 4", "Cat 5", "Die away", "Gone now", "Close"], CHANNEL);
llSetTimerEvent(5.0);
}
listen(integer ch, string name, key id, string msg)
{
llListenRemove(listener);
if (llGetSubString(msg, 0, 3) == "Cat ")
{
asked = msg;
// every option is optional: "minutes" 5-300 in all, "form" 2-60, "centre" <x, y, 0>, "radius" 1000-60000 m,
// "heading" 0-360 (degrees it moves towards), "speed" 0-30 m/s, "rain" TRUE/FALSE
ask = wolfCallAsync("weather.hurricane", ["category", (integer)llGetSubString(msg, 4, -1), "minutes", 40, "form", 10]);
}
else if (msg == "Die away") { asked = msg; ask = wolfCallAsync("weather.hurricane.end", []); }
else if (msg == "Gone now") { asked = msg; ask = wolfCallAsync("weather.hurricane.end", ["now"]); }
else return;
if (ask == NULL_KEY) llOwnerSay(asked + ": " + wolfGetLastError());
}
dataserver(key q, string data)
{
if (q != ask) return;
// "ok|id|category|form s|mature s|decay s|radius|eye|hemisphere", "ok|decaying|seconds left", "ok|gone" or "error: why"
list d = llParseStringKeepNulls(data, ["|"], []);
if (llList2String(d, 0) != "ok") llOwnerSay(asked + ": " + data);
else if (llGetListLength(d) == 9)
llOwnerSay("A category " + llList2String(d, 2) + " hurricane is forming: " + (string)(llList2Integer(d, 3) / 60) + " minutes forming, "
+ (string)(llList2Integer(d, 4) / 60) + " at full strength, " + (string)(llList2Integer(d, 5) / 60) + " dying away; "
+ "reaching " + llList2String(d, 6) + " m from its eye, turning " + llList2String(["clockwise (south)", "anticlockwise (north)"], llList2Integer(d, 8) > 0) + ".");
else llOwnerSay(asked + ": " + data);
llSetTimerEvent(5.0);
show();
}
timer() { show(); }
}
Naval battles (The Caribbean only)
Only The Caribbean answers these names (the wolf-naval plugin). Anywhere else the call is refused, and wolfGetLastError says wolfCall: no plugin function "naval.battle" on this region. Each one acts on the ship or shore cannon the calling script is in (its whole linkset), whoever owns the script.
| Name | Give it | You get back | Who may | What it does |
|---|---|---|---|---|
| naval.battle | ["on", class], ["off"] or ["status"] | [seq, state, health, port guns, starboard guns, sail, turn, struck, protected s, last event], or ["off"] | a script in the ship | The Caribbean only. Puts this ship or shore cannon into battle mode (class, e.g. "frigate_pallas" or "shore_cannon"; an unknown class is refused with the list of known ones), takes it out (it goes off after 60 seconds; firing, or "on" again, cancels that) or reads its state. At most 12 ships and 8 shore cannons in battle mode on the region, and 2 ships and 4 cannons of any one owner. Not for a worn object. |
| naval.fire | [side, elevation, shot] or [side, elevation, shot, captain] | ["fired", guns, "prim,prim,...", seconds until the next is ready] | a script in the ship | The Caribbean only. A broadside: every loaded gun on "port" or "starboard" fires. Elevation in degrees (default 3, held to each gun's limits); shot "round" (the default), "chain" or "grape" (grape only where the region has player damage); captain = the key of the avatar giving the order, counted only while they sit aboard. |
| naval.gun | [gun prim, elevation, traverse, gunner, shot] | ["fired", gun prim, reload seconds, elevation, traverse] | a script in the ship | The Caribbean only. One gun fired by its crew. The gunner's key must be sitting on the object; elevation and traverse are held to that gun's limits. |
| naval.crew | ["gun,gun,...", magazine 0 or 1] | [crewed guns] | a script in the ship | The Caribbean only. Which guns are crewed (a crewed gun reloads in 20 seconds, not 35) and whether the magazine is manned (reloads take a quarter less time). [0] when not in battle mode. |
| naval.status | [] | the same as naval.battle | a script in the ship | The Caribbean only. The state now, at most twice a second (naval.watch tells you of every change). State is off, on, sinking, sunk:seconds, silenced:seconds or switching_off:seconds; health is "section=now/max,..."; each gun side is "ready/total/seconds" (a shore cannon's guns are in the port place, starboard is "0/0/0"). |
| naval.watch | [last seq] | the state as one "|" string (wolfCallAsync) | a script in the ship | The Caribbean only. Answers as soon as anything has changed since that sequence number, or after 240 seconds with nothing new. At most 4 scripts watch one ship. wolfCallAsync only. |
| naval.score | [] | ["hits", n, "sinks", n, "damage_taken", n] or ["off"] | a script in the ship | The Caribbean only. This ship's hits and sinks on others, and the damage it has taken, while in battle mode. |
| naval.booty | [] or [avatar] | "hoard|doubloons|ships sunk|battles won|best haul|rank|season doubloons|season rank" (wolfCallAsync), or "error:why" | any script | The Caribbean only. A resident's booty (the object's owner when no key is given); rank 0 = not on the board yet. Gold doubloons are game treasure, not WT$ or Wolf Credits. wolfCallAsync only; an object may ask once every 10 seconds, and answers are kept a minute. |
| naval.leaderboard | [] or [how many 1-10, "season" or "all"] | "board|season or all|n|rank|name|doubloons|ships sunk|..." (wolfCallAsync), or "error:why" | any script | The Caribbean only. The Top Pirates (default: the top 10 this season; a season is a calendar quarter). Four fields a pirate after the first three. The same board as wolf-grid.com/top-pirates. wolfCallAsync only, once every 10 seconds an object, kept a minute. |
Booty. When a ship strikes or sinks, the ship that did her the most damage takes her prize in gold doubloons (40 + 8 a gun of her class, half as much again for an NPC pirate), shared among everyone sitting aboard: the captain named in naval.fire two shares, every hand one. Her ship then gets a booty event in naval.watch: booty:<beaten ship>;<avatar>,<granted>,<hoard>,<rank>;.... Limits and the board: Top Pirates.
Dogfights (wolf-air)
Only regions with the wolf-air plugin answer these names; it is being tried first on Sandbox and Sandbox 2. Anywhere else the call is refused, and wolfGetLastError says wolfCall: no plugin function "air.dogfight" on this region. Each one acts on the aircraft the script is in. Dogfight mode is a switch on each aircraft, like battle mode on a ship: two aircraft with it on can fight on any region, and an aircraft with it off can never fire or be hit. Nobody can fire or be hit in safe airspace near an airport.
| Name | Give it | You get back | Who may | What it does |
|---|---|---|---|---|
| air.dogfight | ["on", class], ["on", "custom", nose axis 0-3, "x,y,z,type;..."], ["off"] or ["status"] | [seq, state, health, guns, cannon, power, roll, pitch, yaw, lift, fuel leak, fire, protected s, safe, last event], or ["off"] | a script in the aircraft | Puts this aircraft into dogfight mode (any region with wolf-air): only aircraft in dogfight mode can fire on each other or be hit; nobody else is ever touched. "custom" is for ANY aircraft: her hit boxes come from her own size; the nose axis is 0 = +X, 1 = -X, 2 = +Y, 3 = -Y of her root; the guns are muzzle positions in her root's frame with a type mg, hmg or cannon (empty: two machine guns in the nose; at most 8). Off takes 30 seconds and firing cancels it. 16 aircraft a region, 3 an owner. State is off, on, switching_off:seconds, down or wreck:seconds. |
| air.fire | [group "guns", "cannon" or "all", pilot, seconds 0.1-0.5] | ["fired", rounds, guns ammo, cannon ammo, guns heat %, cannon heat %] | a script in the aircraft | A burst from that group. Hold the trigger by calling it 4 times a second with 0.25 seconds each (at most 4 a second). The pilot must be sitting in her. Refused in safe airspace near an airport, out of ammunition, or with the guns jammed (a gun jams at 100 % heat and clears at 40 %: fire short bursts). |
| air.status | [] | the same as air.dogfight | a script in the aircraft | The state now, at most twice a second (air.watch tells you of every change). |
| air.watch | [last seq] | the state as one "|" string (wolfCallAsync) | a script in the aircraft | Answers as soon as anything has changed since that sequence number, or after 240 seconds with nothing new. At most 4 scripts watch one aircraft. The last field is the last event, "kind:words" (hit, hit_given, kill, down, wreck, respawn, fire, engine_dead, pilot_hit, rearmed, dogfight_on, switching_off, dogfight_off). |
| air.bailout | [pilot, parachute key or ""] | ["ok"] | a script in the aircraft | The pilot jumps: stood up and seated on the parachute (an object the aircraft rezzed, described wolfair:parachute, owned by her owner, within 20 m), or just stood up. She is then down: a kill for whoever hit her in the last minute. |
| air.crash | [impact speed m/s] | the state | a script in the aircraft | She has hit the ground. A crash within a minute of being hit is the attacker's kill ("forced down"). She lies as a wreck for 30 seconds, then respawns at home with full health and ammunition, protected for 20 seconds. |
| air.home | [position, rotation] | ["ok"] | a script in the aircraft | Where she respawns (on this region). Without it: where she was when dogfight mode went on. |
| air.score | [] | ["kills", n, "deaths", n, "hits", n, "damage_taken", n] or ["off"] | a script in the aircraft | This aircraft's score while in dogfight mode. |
| air.feed | [last seq] | "seq" then one "seq|unix time|killer|killer plane|victim|victim plane|how" line a kill (wolfCallAsync) | any script | This region's kill feed (the last 30): answered when someone is shot down after that sequence number. Give -1 for everything kept. At most 16 scripts a region wait on it. |
| air.leaderboard | [] or [how many 1-10] | "board|n" then one "rank|name|kills|deaths" line an ace (wolfCallAsync), or "error:why" | any script | The grid's top aces. |
| air.safe | [] | ["safe", 0 or 1, name, metres to the edge] | any script | Whether this object is in safe airspace: 800 m round each of the region's airports, up to 300 m above the ground, and any wolfair:safe|radius|height marker the region owner or a manager has put out. Nobody can fire or be hit there. |
| air.drones | ["on", how many 1-4, "target" or "enemy"], ["off"] or ["status"] | ["on" or "off", how many, "target" or "enemy", in the air, "awake" or "asleep"] | a script owned by the region owner or a manager | Practice drones flown by the region: targets fly a circuit and evade; enemies pursue aircraft in dogfight mode and fire short bursts. They only fly while someone is on the region and an aircraft is in dogfight mode, and cost nobody anything (no AI credits: they are rule-based). Also "/74 drones on|off|status" in local chat. |
| air.traffic | ["on"], ["off"] or ["status"] | ["on" or "off", planes in the air, "awake" or "asleep"] | status: any script; on and off: a script owned by the region owner or a manager | Peaceful air traffic on autopilot passing over the region, with navigation and landing lights at night. Never in a dogfight, never hit. Also "/74 traffic on|off|status" in local chat. |
Make any aircraft dogfight-ready. Drop the Wolf Dogfight script (below) into the root prim of any aircraft - one you bought, made or were given. It works beside her own flight script, which keeps flying her. Touch her (as the owner) and the setup walks you through it: sit in the pilot's seat, say which way her nose points (or let it learn on your first flight), choose where her guns are (prims named gun..., two in the nose, four in the wings or a nose cannon), the fire key (the left mouse button in mouselook, Shift+arrows or chat) and, if you like, the script that flies her. Then say dogfight on, or press DOGFIGHT on the WolfViewer flight deck. Add an object called "Wolf Parachute" (with the parachute script) to her contents so the pilot can say bail.
What it cannot do. Damage cannot slow another maker's flight script: with the kit she smokes and burns, the panel shows her damage, and she flies at full strength until she is shot down. Then the kit stops her flight script, lets her fall, and starts the script again when she respawns at home. Our own Wolf Air planes fly the Wolf flight model, so their damage is felt.
Wolf Dogfight: makes any aircraft dogfight-ready (drop it in her root prim and touch her) (wolf_dogfight.lsl, 966 lines: show or hide)
// Wolf Dogfight 1.0.0 - makes ANY aircraft able to dogfight on Wolf Territories (the wolf-air plugin), 2026-11-01.
// Copyright (C) 2026 IntelligentWolf Ltd. Free to use in any aircraft on Wolf Territories.
//
// Paul 2026-11-01: "need extra scripting so someone can enable any aircraft". Drop this script into the ROOT prim of your
// aircraft (bought, home-made, from any creator). It works BESIDE her own flight script, which keeps flying her.
// Design: region-terrain-work/aircraft-dogfight-2026-11-01/a-research/DESIGN.md s3.2 (calls), s3.5 (bail out), s4 (this kit),
// s2 (the WolfAir1 lines to the WolfViewer dogfight panel). Calls answered by the wolf-air plugin (c-wolf-air/src).
//
// Touch her (owner) the first time: a short setup walks you through the pilot's seat, which way her nose points, where the
// guns are, the fire key and (optionally) her flight script. Then say "dogfight on" (or press DOGFIGHT on the flight deck).
//
// Chat (channel 0 from the owner or the pilot, or on her channel, 7 unless set):
// dogfight on | dogfight off | status | fire (one second of fire) | bail | guns | cannon | all guns | home here | setup
// Keys: the fire key you chose in setup (the left mouse button in mouselook by default). Her own flight script still gets
// every key: OpenSim sends a control event to EVERY script that took that key (ScenePresence.cs:6534-6590
// SendControlsToScripts loops over all of them), whatever its pass_on.
//
// Our own Wolf Air planes carry this same script beside their helm (wolf_plane_helm*). The generator sets the linkset data
// "wolfair_class" (her class) and the link messages below connect the two (README.md "Link protocol"):
// 7300 helm -> kit "fire|1" / "fire|0", "crash|<impact m/s>", "bail"
// 7301 kit -> helm "factors|<power>|<roll>|<pitch>|<yaw>|<lift>|<fire 0/1>|<state>"
// 7302 kit -> helm "down" / "respawn|<pos>|<rot>"
// 7304 kit -> helm "hud|ammo=<g>/<c>|heat=<g>/<c>|dmg=<8 parts %>|fire=<0/1>|smoke=<0/1>|df=<state>|safe=<0/1>"
// With a helm aboard the helm speaks "hi" and "st" to the viewer (it knows speed, throttle, gear); this script then speaks
// only the "hit", "kill", "down" and "respawn" lines. Without one, it speaks all of them.
string VER = "1.0.0";
integer HELLO_CH = 7; // DESIGN s2: the viewer shouts "hud hello 1" on channel 7 until "hi" names her channel
string HUD = "WolfAir1|";
float BURST = 0.25; // DESIGN s3.3: a burst is the trigger held for 0.25 s ...
float FIRE_TICK = 0.26; // ... and air.fire is called at most 4 times a second (AirRules.MAX_FIRE_PER_S = 4)
float FALL_TICK = 0.2; // the fall's keyframes: a 0.2 s tick and 0.3 s frames (hms_wolfy_helm_template.lsl DT/FRAME)
float FALL_FRAME = 0.3;
float TERMINAL = 70.0; // GAME TUNING: a falling wreck goes no faster than this (m/s)
float SPIN = 1.0; // GAME TUNING: rad/s of yaw while she spins down
float LEARN_MS = 10.0; // DESIGN s4 step 2: the nose is learned above 10 m/s
list SECTIONS = ["engine", "fuel", "cockpit", "fuselage", "wing_left", "wing_right", "tail", "fin"]; // AirRules.SECTIONS
// ── settings (linkset data, so they survive a re-rez) ──
string gClass; // "" = custom (her own size), else our generator's class (linkset data "wolfair_class")
integer gPilotLink; // the pilot seat prim (0 = the first to sit)
string gAxis; // "auto" or "0".."3" = +X, -X, +Y, -Y (the nose axis air.dogfight custom takes)
string gGuns; // "x,y,z,type;..." in her root's frame (types mg, hmg, cannon)
string gFireKey; // "ml" (mouselook left button), "shift" (Shift+Left/Right), "chat"
string gScript; // her flight script (stopped when she is shot down, reset at the respawn), "" = every other script here
integer gCh; // her channel (7 unless set)
string gGroup; // "all", "guns", "cannon"
// ── state ──
key gOwner;
key gPilot = NULL_KEY;
integer gHelm; // a Wolf helm is in this prim
integer gOn; // dogfight mode wanted
string gState = "off";
integer gSeq = -1;
string gHealth = "";
string gGunsSt = "0/0/0";
string gCannonSt = "0/0/0";
float gPower = 1.0;
float gRoll = 1.0;
string gRollS = "1"; // as the plugin wrote it: "L" or "R" after it = she rolls towards that weaker wing (AirEngine.WatchData)
float gPitch = 1.0;
float gYaw = 1.0;
float gLift = 1.0;
integer gFire;
integer gSafe;
integer gSmoke;
string gEvent = "";
key gWatchQ = NULL_KEY;
float gWatchAt;
float gWatchRetry;
integer gTrigger; // the fire key is held (or the panel's FIRE)
float gTriggerUntil; // chat "fire": until then
float gLastWhy; // when the pilot was last told why a burst was refused
list gHud; // avatars who said hello (seated)
integer gMenuCh;
integer gMenuL;
integer gZeroL;
integer gZeroPilotL;
integer gChL;
integer gHelloL;
string gStep = ""; // the setup step on show
list gScriptsShown;
vector gHomeP;
rotation gHomeR;
integer gHaveHome;
// the fall (no helm aboard)
integer gFalling;
vector gFallV;
list gStopped; // scripts this kit stopped
list gGunLinks; // gun prims whose +Z lies along her nose (they draw particle tracers)
list gGlowSave; // [link, glow] while gun prims are shown in setup
integer gEvSeq = -1; // the seq whose event was last acted on
integer gTracersOn;
float gLastPub;
float gLastHitTold;
// ─────────────────────────────────────────────────────────────── small helpers
Tell(key a, string s) { if (a != NULL_KEY) llRegionSayTo(a, 0, s); }
TellBoth(string s) { if (gPilot != NULL_KEY && gPilot != gOwner) Tell(gPilot, s); Tell(gOwner, s); }
string Why() {
string e = wolfGetLastError();
if (llSubStringIndex(e, "no plugin function") != -1)
return "this region has no dogfight plugin yet (wolf-air) - dogfights work only where it is loaded";
if (e == "") e = "the guns did not answer";
return e;
}
string Lsd(string k, string dflt) { string v = llLinksetDataRead(k); if (v == "") return dflt; return v; }
LoadSettings() {
gClass = llLinksetDataRead("wolfair_class");
gPilotLink = (integer)Lsd("wolfair_pilot", "0");
gAxis = Lsd("wolfair_axis", "auto");
gGuns = llLinksetDataRead("wolfair_guns");
gFireKey = Lsd("wolfair_firekey", "ml");
gScript = llLinksetDataRead("wolfair_script");
gCh = (integer)Lsd("wolfair_ch", "7");
gGroup = Lsd("wolfair_group", "all");
string h = llLinksetDataRead("wolfair_home");
gHaveHome = FALSE;
if (h != "") {
list p = llParseString2List(h, ["|"], []);
gHomeP = (vector)llList2String(p, 0);
gHomeR = (rotation)llList2String(p, 1);
gHaveHome = TRUE;
}
}
integer Ready() {
if (gClass != "") return TRUE; // our own planes: the generator set everything
return llLinksetDataRead("wolfair_ready") == "1";
}
integer IsHelmHere() {
integer n = llGetInventoryNumber(INVENTORY_SCRIPT);
integer i;
for (i = 0; i < n; ++i)
if (llSubStringIndex(llGetInventoryName(INVENTORY_SCRIPT, i), "wolf_plane_helm") == 0) return TRUE;
return FALSE;
}
// The avatars sitting on her: link numbers after the prims.
integer Seated(key a) {
integer n = llGetNumberOfPrims();
integer l;
for (l = llGetObjectPrimCount(llGetKey()) + 1; l <= n; ++l) if (llGetLinkKey(l) == a) return TRUE;
return FALSE;
}
key FindPilot() {
if (gPilotLink > 0) {
key k = llAvatarOnLinkSitTarget(gPilotLink);
if (k != NULL_KEY) return k;
return NULL_KEY;
}
integer first = llGetObjectPrimCount(llGetKey()) + 1;
if (llGetNumberOfPrims() >= first) return llGetLinkKey(first);
return NULL_KEY;
}
// The nose in her root's frame (the axis air.dogfight custom is told).
vector Nose() {
if (gAxis == "1") return <-1.0, 0.0, 0.0>;
if (gAxis == "2") return <0.0, 1.0, 0.0>;
if (gAxis == "3") return <0.0, -1.0, 0.0>;
return <1.0, 0.0, 0.0>;
}
string F2(float f) { return (string)((float)llRound(f * 100.0) / 100.0); }
string V3(vector v) { return F2(v.x) + "," + F2(v.y) + "," + F2(v.z); }
// ─────────────────────────────────────────────────────────────── guns
// Prims whose name starts "gun": their positions in the root's frame; their description names the type (mg, hmg, cannon;
// anything else is mg). Only prims whose own +Z lies along the nose draw particle tracers (a particle source fires along
// its prim's +Z: llviewerpartsource.cpp, part_dir_vector (0,0,1) * the prim's rotation).
string GunPrims() {
string out = "";
integer n = llGetNumberOfPrims();
integer l;
integer count = 0;
for (l = 2; l <= n && count < 8; ++l) { // AirRules.MAX_CUSTOM_GUNS = 8
string nm = llToLower(llGetLinkName(l));
if (llGetSubString(nm, 0, 2) == "gun") {
list p = llGetLinkPrimitiveParams(l, [PRIM_POS_LOCAL, PRIM_DESC]);
string t = llToLower(llStringTrim(llList2String(p, 1), STRING_TRIM));
if (t != "hmg" && t != "cannon") t = "mg";
if (out != "") out += ";";
out += V3(llList2Vector(p, 0)) + "," + t;
++count;
}
}
return out;
}
FindGunLinks() {
gGunLinks = [];
integer n = llGetNumberOfPrims();
integer l;
vector nose = Nose();
for (l = 2; l <= n; ++l) {
if (llGetSubString(llToLower(llGetLinkName(l)), 0, 2) == "gun") {
rotation r = llList2Rot(llGetLinkPrimitiveParams(l, [PRIM_ROT_LOCAL]), 0);
if (<0.0, 0.0, 1.0> * r * nose > 0.94) gGunLinks += [l]; // within 20 degrees of the nose
}
}
}
// Presets from her own bounding box (llGetBoundingBox: the linkset's box in the root's frame, LSL_Api.cs:11694-11697).
string Preset(string which) {
list bb = llGetBoundingBox(llGetKey());
vector mn = llList2Vector(bb, 0);
vector mx = llList2Vector(bb, 1);
vector nose = Nose();
vector side = <0.0, 0.0, 1.0> % nose; // her left
float front = mx * nose;
if (mn * nose > front) front = mn * nose; // the far end along the nose (the nose may point down an axis)
float half = llFabs((mx - mn) * side) * 0.5;
float midz = (mn.z + mx.z) * 0.5;
vector c = (mn + mx) * 0.5;
vector ahead = nose * front;
vector up = <0.0, 0.0, midz>;
vector centre = c - nose * (c * nose) - <0.0, 0.0, c.z>; // the centre line across her, at the root's height
if (which == "nose2")
return V3(centre + ahead * 0.85 + side * 0.25 + up) + ",mg;" + V3(centre + ahead * 0.85 - side * 0.25 + up) + ",mg";
if (which == "wing4")
return V3(centre + ahead * 0.2 + side * half * 0.35 + up) + ",mg;" + V3(centre + ahead * 0.2 + side * half * 0.5 + up) + ",mg;"
+ V3(centre + ahead * 0.2 - side * half * 0.35 + up) + ",mg;" + V3(centre + ahead * 0.2 - side * half * 0.5 + up) + ",mg";
return V3(centre + ahead * 0.95 + up) + ",cannon";
}
integer CountGuns(string type) {
list g = llParseString2List(gGuns, [";"], []);
integer i;
integer n = 0;
for (i = 0; i < llGetListLength(g); ++i) {
string t = llList2String(llParseString2List(llList2String(g, i), [","], []), 3);
if ((type == "cannon") == (t == "cannon")) ++n;
}
return n;
}
// The setup preview: "Wolf gun marker" objects (temporary red spheres) at each gun if the marker is in her contents,
// otherwise the gun prims glow for 20 s.
ShowGuns(string guns) {
list g = llParseString2List(guns, [";"], []);
integer i;
integer have = llGetInventoryType("Wolf gun marker") == INVENTORY_OBJECT;
for (i = 0; i < llGetListLength(g) && have; ++i) {
list p = llParseString2List(llList2String(g, i), [","], []);
vector local = <(float)llList2String(p, 0), (float)llList2String(p, 1), (float)llList2String(p, 2)>;
vector at = llGetPos() + local * llGetRot();
if (llVecDist(at, llGetPos()) < 10.0) llRezObject("Wolf gun marker", at, ZERO_VECTOR, ZERO_ROTATION, 20);
}
if (!have) {
integer n = llGetNumberOfPrims();
integer l;
for (l = 2; l <= n; ++l)
if (llGetSubString(llToLower(llGetLinkName(l)), 0, 2) == "gun") {
gGlowSave += [l, llList2Float(llGetLinkPrimitiveParams(l, [PRIM_GLOW, 0]), 0)];
llSetLinkPrimitiveParamsFast(l, [PRIM_GLOW, ALL_SIDES, 0.6]);
}
}
}
UnshowGuns() {
integer i;
for (i = 0; i < llGetListLength(gGlowSave); i += 2)
llSetLinkPrimitiveParamsFast(llList2Integer(gGlowSave, i), [PRIM_GLOW, ALL_SIDES, llList2Float(gGlowSave, i + 1)]);
gGlowSave = [];
}
// ─────────────────────────────────────────────────────────────── particles
Tracers(integer on) {
if (on == gTracersOn) return;
gTracersOn = on;
integer i;
for (i = 0; i < llGetListLength(gGunLinks); ++i) {
integer l = llList2Integer(gGunLinks, i);
if (!on) llLinkParticleSystem(l, []);
else llLinkParticleSystem(l, [
PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_FOLLOW_VELOCITY_MASK | PSYS_PART_INTERP_COLOR_MASK,
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_ANGLE_CONE,
PSYS_SRC_ANGLE_BEGIN, 0.0, PSYS_SRC_ANGLE_END, 0.01,
PSYS_SRC_BURST_SPEED_MIN, 250.0, PSYS_SRC_BURST_SPEED_MAX, 255.0, // llpartdata.cpp:255-256 unpackFixed 8.8: 255 is the most
PSYS_SRC_BURST_RATE, 0.08, PSYS_SRC_BURST_PART_COUNT, 1,
PSYS_PART_MAX_AGE, 1.2,
PSYS_PART_START_SCALE, <0.12, 2.5, 0.0>, PSYS_PART_END_SCALE, <0.08, 2.0, 0.0>,
PSYS_PART_START_COLOR, <1.0, 0.85, 0.45>, PSYS_PART_END_COLOR, <1.0, 0.55, 0.2>,
PSYS_PART_START_ALPHA, 1.0, PSYS_PART_END_ALPHA, 0.6,
PSYS_PART_START_GLOW, 0.4, PSYS_PART_END_GLOW, 0.2]);
}
}
// Smoke from a damaged engine, flames when she burns (on the root; her own particles come back when she is mended only if
// her own scripts set them again - README "Limits").
integer gPartsShown = -1;
Damage(integer smoke, integer fire) {
integer want = smoke + fire * 2;
if (want == gPartsShown) return;
gPartsShown = want;
if (want == 0) { llParticleSystem([]); return; }
vector c1 = <0.35, 0.35, 0.35>;
vector c2 = <0.15, 0.15, 0.15>;
float glow = 0.0;
if (fire) { c1 = <1.0, 0.55, 0.15>; c2 = <0.2, 0.2, 0.2>; glow = 0.15; }
llParticleSystem([
PSYS_PART_FLAGS, PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK | PSYS_PART_WIND_MASK,
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,
PSYS_SRC_BURST_SPEED_MIN, 0.2, PSYS_SRC_BURST_SPEED_MAX, 0.8,
PSYS_SRC_BURST_RATE, 0.05, PSYS_SRC_BURST_PART_COUNT, 3, PSYS_SRC_BURST_RADIUS, 0.5,
PSYS_PART_MAX_AGE, 4.0,
PSYS_PART_START_SCALE, <0.8, 0.8, 0.0>, PSYS_PART_END_SCALE, <3.5, 3.5, 0.0>,
PSYS_PART_START_COLOR, c1, PSYS_PART_END_COLOR, c2,
PSYS_PART_START_ALPHA, 0.8, PSYS_PART_END_ALPHA, 0.0,
PSYS_PART_START_GLOW, glow, PSYS_PART_END_GLOW, 0.0]);
}
MuzzleFlash() {
if (gGunLinks != []) return;
llParticleSystem([
PSYS_PART_FLAGS, PSYS_PART_EMISSIVE_MASK | PSYS_PART_INTERP_COLOR_MASK,
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,
PSYS_SRC_BURST_SPEED_MIN, 0.5, PSYS_SRC_BURST_SPEED_MAX, 1.5, PSYS_SRC_BURST_RADIUS, 0.2,
PSYS_SRC_BURST_RATE, 0.05, PSYS_SRC_BURST_PART_COUNT, 2, PSYS_SRC_MAX_AGE, 0.3,
PSYS_PART_MAX_AGE, 0.15,
PSYS_PART_START_SCALE, <0.6, 0.6, 0.0>, PSYS_PART_END_SCALE, <0.2, 0.2, 0.0>,
PSYS_PART_START_COLOR, <1.0, 0.85, 0.4>, PSYS_PART_END_COLOR, <1.0, 0.45, 0.1>,
PSYS_PART_START_GLOW, 0.5, PSYS_PART_END_GLOW, 0.0]);
gPartsShown = -1;
}
// ─────────────────────────────────────────────────────────────── the viewer's dogfight panel (WolfAir1, DESIGN s2)
Line(string s) {
integer i;
for (i = 0; i < llGetListLength(gHud); ++i) llRegionSayTo(llList2Key(gHud, i), 0, HUD + s);
}
string Pct() {
list h = llParseString2List(gHealth, [","], []);
list out = [];
integer s;
for (s = 0; s < llGetListLength(SECTIONS); ++s) {
integer pct = 100;
integer i;
for (i = 0; i < llGetListLength(h); ++i) {
list kv = llParseString2List(llList2String(h, i), ["=", "/"], []);
if (llList2String(kv, 0) == llList2String(SECTIONS, s)) {
float m = (float)llList2String(kv, 2);
if (m > 0.0) pct = llRound(100.0 * (float)llList2String(kv, 1) / m);
}
}
out += [pct];
}
return llDumpList2String(out, ",");
}
string Field(string st, integer i) { return llList2String(llParseStringKeepNulls(st, ["/"], []), i); }
string HudFields() {
return "ammo=" + Field(gGunsSt, 0) + "/" + Field(gCannonSt, 0) + "|heat=" + Field(gGunsSt, 1) + "/" + Field(gCannonSt, 1)
+ "|dmg=" + Pct() + "|fire=" + (string)gFire + "|smoke=" + (string)gSmoke + "|df=" + gState + "|safe=" + (string)gSafe;
}
Hi(key a) {
if (gHelm) return; // the helm says hi (it knows her flight numbers)
string cls = gClass;
if (cls == "") cls = "custom";
llRegionSayTo(a, 0, HUD + "hi|ver=1|plane=" + llGetObjectName() + "|class=" + cls + "|ch=" + (string)gCh
+ "|vs=0|vne=0|gmax=0|guns=" + (string)CountGuns("guns") + "|cannon=" + (string)CountGuns("cannon") + "|wolfair=1");
}
Publish() {
gLastPub = llGetTime();
gSmoke = (gPower < 1.0) || gState == "down";
llMessageLinked(LINK_THIS, 7301, "factors|" + F2(gPower) + "|" + gRollS + "|" + F2(gPitch) + "|" + F2(gYaw) + "|" + F2(gLift)
+ "|" + (string)gFire + "|" + gState, NULL_KEY);
if (gHelm) llMessageLinked(LINK_THIS, 7304, "hud|" + HudFields(), NULL_KEY);
else if (gHud != []) Line("st|" + HudFields() + "|say=");
if (gState == "off") Damage(0, 0);
else Damage(gSmoke, gFire);
}
// ─────────────────────────────────────────────────────────────── the plugin (wolf-air)
Watch() {
if (!gOn && gState == "off") { gWatchQ = NULL_KEY; return; }
gWatchQ = wolfCallAsync("air.watch", [gSeq]);
gWatchAt = llGetTime();
if (gWatchQ == NULL_KEY) gWatchRetry = llGetTime() + 15.0;
}
// STATE: seq|state|health|guns|cannon|power|roll|pitch|yaw|lift|fuel_leak|fire|protected s|safe|last event (DESIGN s3.2)
Absorb(string data) {
list p = llParseStringKeepNulls(data, ["|"], []);
if (llGetListLength(p) < 15) return;
string was = gState;
gSeq = (integer)llList2String(p, 0);
gState = llList2String(p, 1);
gHealth = llList2String(p, 2);
gGunsSt = llList2String(p, 3);
gCannonSt = llList2String(p, 4);
gPower = (float)llList2String(p, 5);
gRollS = llList2String(p, 6);
gRoll = (float)gRollS;
gPitch = (float)llList2String(p, 7);
gYaw = (float)llList2String(p, 8);
gLift = (float)llList2String(p, 9);
gFire = (integer)llList2String(p, 11);
gSafe = (integer)llList2String(p, 13);
string ev = llList2String(p, 14);
// a new event whenever the seq moves on (two hits can read the same); the answer to "on" carries seq -1
if (gSeq > gEvSeq && ev != "") { gEvSeq = gSeq; gEvent = ev; Happened(ev); }
if (gState != was) Changed(was);
Publish();
}
// The event's own words: "<kind>:<text>". Kinds read here: hit, down, respawn (README "Event kinds").
Happened(string ev) {
integer c = llSubStringIndex(ev, ":");
string kind = ev;
string text = "";
if (c != -1) { kind = llGetSubString(ev, 0, c - 1); text = llGetSubString(ev, c + 1, -1); }
if (kind == "hit") {
// "<part> hit for <n> by <name>"
list w = llParseString2List(text, [" "], []);
string by = "";
integer b = llSubStringIndex(text, " by ");
if (b != -1) by = llGetSubString(text, b + 4, -1);
string dmg = "";
integer f = llListFindList(w, ["for"]);
if (f != -1) dmg = llList2String(w, f + 1);
Line("hit|by=" + by + "|part=" + llList2String(w, 0) + "|dmg=" + dmg);
}
if (kind == "kill") {
// "you shot down <name> (<how>)" (AirEngine.cs ShotDown). The region's whole kill feed reaches every Wolf viewer as
// the plugin's own "WolfAir" message (AirEngine.AddFeed W.Broadcast), so this line is only her own kills.
string v = text;
if (llGetSubString(v, 0, 13) == "you shot down ") v = llGetSubString(v, 14, -1);
string how = "";
integer o = llSubStringIndex(v, " (");
if (o != -1) { how = llGetSubString(v, o + 2, -2); v = llGetSubString(v, 0, o - 1); }
Line("kill|killer=" + llGetObjectName() + "|victim=" + v + "|how=" + how);
}
if (gPilot == NULL_KEY || kind == "fired" || kind == "hit_given") return;
if (kind == "hit") { // the dogfight panel shows hits; in chat at most one every 2 s
if (gHud != [] || llGetTime() < gLastHitTold + 2.0) return;
gLastHitTold = llGetTime();
}
Tell(gPilot, llGetObjectName() + ": " + text);
}
Changed(string was) {
if (gState == "down") {
// "down:shot down by <name>: <how>" (AirEngine.cs Event(t, "down", ...))
string by = "";
integer b = llSubStringIndex(gEvent, " by ");
if (b != -1) {
by = llGetSubString(gEvent, b + 4, -1);
integer c = llSubStringIndex(by, ": ");
if (c != -1) by = llGetSubString(by, 0, c - 1);
}
Line("down|by=" + by);
Trigger(FALSE);
if (gHelm) llMessageLinked(LINK_THIS, 7302, "down", NULL_KEY);
else FallStart();
}
if (llGetSubString(gState, 0, 4) == "wreck") {
FallStop();
UnsitAll();
}
if (gState == "on" && (llGetSubString(was, 0, 4) == "wreck" || was == "down")) Respawn();
if (gState == "off" && was != "off") { gOn = FALSE; llLinksetDataWrite("wolfair_df", "off"); Trigger(FALSE); }
}
Dogfight(key a, integer on, integer quiet) {
if (a != gOwner && a != gPilot && !quiet) { Tell(a, "Only her owner or her pilot may switch dogfight mode."); return; }
list r;
if (on) {
if (!Ready()) { Tell(a, "She is not set up for dogfights yet - touch her (owner) to set her up."); return; }
if (gClass != "") r = wolfCall("air.dogfight", ["on", gClass]);
else {
if (gAxis == "auto") { Tell(a, "Fly her straight for a few seconds first so I can learn which way her nose points (or pick it in the setup)."); return; }
r = wolfCall("air.dogfight", ["on", "custom", (integer)gAxis, gGuns]);
}
}
else r = wolfCall("air.dogfight", ["off"]);
if (r == []) { if (!quiet) Tell(a, "Dogfight mode: " + Why() + "."); return; }
gOn = on;
if (on) llLinksetDataWrite("wolfair_df", "on"); else llLinksetDataWrite("wolfair_df", "off");
if (llGetListLength(r) >= 15) Absorb(llDumpList2String(["-1"] + llList2List(r, 1, -1), "|"));
else { gState = "off"; Publish(); }
gSeq = -1;
Watch();
if (on) {
if (!gHaveHome) SetHome();
else wolfCall("air.home", [gHomeP, gHomeR]);
FindGunLinks();
if (!quiet) Tell(a, llGetObjectName() + ": DOGFIGHT MODE ON. Any aircraft in dogfight mode may now fire on her, and she on them "
+ "(never inside safe airspace near an airport). " + FireHelp());
}
else if (!quiet) Tell(a, llGetObjectName() + ": dogfight mode goes off in 30 seconds (firing cancels it).");
TakeKeys();
Tick();
}
string FireHelp() {
if (gFireKey == "ml") return "Fire: the left mouse button in mouselook (M), or say fire.";
if (gFireKey == "shift") return "Fire: Shift+Left or Shift+Right, or say fire.";
return "Fire: say fire.";
}
SetHome() {
gHomeP = llGetPos();
gHomeR = llGetRot();
gHaveHome = TRUE;
llLinksetDataWrite("wolfair_home", (string)gHomeP + "|" + (string)gHomeR);
wolfCall("air.home", [gHomeP, gHomeR]);
}
Burst() {
if (gPilot == NULL_KEY || !Seated(gPilot)) { Trigger(FALSE); return; }
list r = wolfCall("air.fire", [gGroup, gPilot, BURST]);
if (r == []) {
if (llGetTime() > gLastWhy + 3.0) { Tell(gPilot, "Guns: " + Why() + "."); gLastWhy = llGetTime(); }
Tracers(FALSE);
return;
}
// ["fired", rounds, guns ammo, cannon ammo, guns heat %, cannon heat %]
if ((integer)llList2String(r, 1) > 0) { Tracers(TRUE); MuzzleFlash(); }
else Tracers(FALSE);
gGunsSt = llList2String(r, 2) + "/" + llList2String(r, 4) + "/" + Field(gGunsSt, 2);
gCannonSt = llList2String(r, 3) + "/" + llList2String(r, 5) + "/" + Field(gCannonSt, 2);
if (llGetTime() > gLastPub + 0.5) Publish(); // the panel's ammo and heat while firing, twice a second
}
Trigger(integer on) {
if (on == gTrigger) return;
gTrigger = on;
if (!on) { Tracers(FALSE); Publish(); }
Tick();
}
Bail() {
if (gPilot == NULL_KEY) return;
if (gState == "off" || llGetSubString(gState, 0, 4) == "wreck") {
llUnSit(gPilot); // nothing to fight about: she just steps out
return;
}
if (llGetInventoryType("Wolf Parachute") == INVENTORY_OBJECT) {
// above the cockpit, inside llRezObject's 10 m reach
llRezObject("Wolf Parachute", llGetPos() + <0.0, 0.0, 3.0>, ZERO_VECTOR, ZERO_ROTATION, 1);
return; // object_rez sends air.bailout with its key
}
BailWith("");
}
BailWith(string chute) {
list r = wolfCall("air.bailout", [gPilot, chute]);
if (r == []) Tell(gPilot, "Bail out: " + Why() + ".");
}
// ─────────────────────────────────────────────────────────────── shot down, without a Wolf helm aboard
StopFlight() {
gStopped = [];
integer n = llGetInventoryNumber(INVENTORY_SCRIPT);
integer i;
string me = llGetScriptName();
for (i = 0; i < n; ++i) {
string s = llGetInventoryName(INVENTORY_SCRIPT, i);
if (s != me && (gScript == "" || s == gScript) && llGetScriptState(s)) {
llSetScriptState(s, FALSE);
gStopped += [s];
}
}
}
StartFlight() {
integer i;
for (i = 0; i < llGetListLength(gStopped); ++i) {
string s = llList2String(gStopped, i);
if (llGetInventoryType(s) == INVENTORY_SCRIPT) {
if (s == gScript) llResetOtherScript(s); // her flight script starts afresh, parked
llSetScriptState(s, TRUE);
}
}
gStopped = [];
}
FallStart() {
gFallV = llGetVel();
StopFlight();
llSetStatus(STATUS_PHYSICS, FALSE);
gFalling = TRUE;
Tick();
}
FallStop() {
if (!gFalling) return;
gFalling = FALSE;
llSetKeyframedMotion([], []);
}
FallStep() {
vector pos = llGetPos();
gFallV.z -= 9.80665 * FALL_TICK;
if (llVecMag(gFallV) > TERMINAL) gFallV = llVecNorm(gFallV) * TERMINAL;
vector move = gFallV * FALL_FRAME;
vector next = pos + move;
vector size = osGetRegionSize(); // OSSL_Api.cs:3663 osGetRegionSize, CheckThreatLevel() with no level: always allowed
if (next.x < 2.0 || next.y < 2.0 || next.x > size.x - 2.0 || next.y > size.y - 2.0) { gFallV.x = 0.0; gFallV.y = 0.0; move.x = 0.0; move.y = 0.0; next = pos + move; }
float floor = llGround(move);
float w = llWater(move);
if (w > floor) floor = w;
float bottom = llList2Vector(llGetBoundingBox(llGetKey()), 0) * <0.0, 0.0, 1.0>;
if (next.z + bottom <= floor) {
float impact = llVecMag(gFallV);
llSetKeyframedMotion([], []);
gFalling = FALSE;
llSetRegionPos(<next.x, next.y, floor - bottom>);
list r = wolfCall("air.crash", [impact]);
if (r != []) Absorb(llDumpList2String(["-1"] + llList2List(r, 1, -1), "|"));
return;
}
rotation spin = llEuler2Rot(<0.0, 0.0, SPIN * FALL_FRAME>);
llSetKeyframedMotion([move, spin, FALL_FRAME], []);
}
UnsitAll() {
integer n = llGetNumberOfPrims();
integer l;
for (l = n; l > llGetObjectPrimCount(llGetKey()); --l) llUnSit(llGetLinkKey(l));
}
Respawn() {
FallStop();
Damage(0, 0);
Line("respawn");
if (gHelm) { llMessageLinked(LINK_THIS, 7302, "respawn|" + (string)gHomeP + "|" + (string)gHomeR, NULL_KEY); return; }
if (gHaveHome) { llSetRegionPos(gHomeP); llSetRot(gHomeR); }
StartFlight();
}
// ─────────────────────────────────────────────────────────────── keys and the timer
TakeKeys() {
if (gPilot == NULL_KEY || gFireKey == "chat") return;
if (llGetPermissionsKey() == gPilot && (llGetPermissions() & PERMISSION_TAKE_CONTROLS)) Grab();
else llRequestPermissions(gPilot, PERMISSION_TAKE_CONTROLS);
}
Grab() {
integer c = CONTROL_ML_LBUTTON;
if (gFireKey == "shift") c = CONTROL_LEFT | CONTROL_RIGHT;
// accept, do not pass on. NOT pass_on TRUE: with accept and pass_on both TRUE OpenSim CLEARS the avatar's ignored
// controls for every key (ScenePresence.cs:6401-6403 IgnoredControls = CONTROL_ZERO), undoing her own flight script's
// llTakeControls(..., TRUE, FALSE). Every script that took a key still gets it (SendControlsToScripts).
llTakeControls(c, TRUE, FALSE);
}
Tick() {
float t = 0.0;
if (gOn || gState != "off") t = 5.0;
if (gAxis == "auto" && gClass == "" && gPilot != NULL_KEY) t = 1.0;
if ((gTrigger || llGetTime() < gTriggerUntil) && gState == "on") t = FIRE_TICK;
if (gFalling) t = FALL_TICK;
llSetTimerEvent(t);
}
Learn() {
vector v = llGetVel() / llGetRot(); // her velocity in her own frame
if (llVecMag(<v.x, v.y, 0.0>) < LEARN_MS) return;
if (llFabs(v.x) >= llFabs(v.y)) { if (v.x > 0.0) gAxis = "0"; else gAxis = "1"; }
else { if (v.y > 0.0) gAxis = "2"; else gAxis = "3"; }
llLinksetDataWrite("wolfair_axis", gAxis);
Tell(gOwner, llGetObjectName() + ": I have learned which way her nose points (" + llList2String(["+X", "-X", "+Y", "-Y"], (integer)gAxis) + "). Dogfight mode can go on now.");
Tick();
}
Pilot() {
key p = FindPilot();
if (p == gPilot) return;
if (gZeroPilotL) { llListenRemove(gZeroPilotL); gZeroPilotL = 0; }
gPilot = p;
Trigger(FALSE);
if (p != NULL_KEY) {
if (p != gOwner) gZeroPilotL = llListen(0, "", p, "");
TakeKeys();
if (Ready() && gState == "off") Tell(p, llGetObjectName() + " can dogfight: say \"dogfight on\". " + FireHelp());
}
Tick();
}
// ─────────────────────────────────────────────────────────────── the setup helper (paul-games-need-setup-helper)
Menu(string step, string text, list buttons) {
gStep = step;
if (gMenuL) llListenRemove(gMenuL);
gMenuL = llListen(gMenuCh, "", gOwner, "");
llDialog(gOwner, text, buttons, gMenuCh);
}
SetupStart() {
Menu("pilot", "Let's get " + llGetObjectName() + " ready for dogfights!\n\nStep 1 of 5: sit in the PILOT's seat, then press \"Pilot seat\".",
["Pilot seat", "Skip", "Cancel"]);
}
SetupNose() {
Menu("nose", "Step 2 of 5: which way does her nose point?\n\"Auto\" learns it the first time you fly her faster than 10 m/s.",
["Auto", "+X", "-X", "+Y", "-Y", "Back"]);
}
SetupGuns() {
integer prims = llGetListLength(llParseString2List(GunPrims(), [";"], []));
string t = "Step 3 of 5: where are her guns?";
if (prims > 0) t += "\nShe has " + (string)prims + " prims named gun... (type from each one's description: mg, hmg or cannon).";
Menu("guns", t, ["Gun prims", "2 in the nose", "4 in the wings", "Nose cannon", "Back"]);
}
SetupGunsCheck(string guns) {
gGuns = guns;
ShowGuns(guns);
Menu("gunscheck", "The guns are shown (red markers or glowing prims) for 20 seconds. Is that right?", ["Yes", "Back"]);
}
SetupFireKey() {
Menu("firekey", "Step 4 of 5: the fire key.", ["Mouse (mouselook)", "Shift+arrows", "Chat only", "Back"]);
}
SetupScript() {
gScriptsShown = [];
integer n = llGetInventoryNumber(INVENTORY_SCRIPT);
integer i;
for (i = 0; i < n && llGetListLength(gScriptsShown) < 9; ++i) {
string s = llGetInventoryName(INVENTORY_SCRIPT, i);
if (s != llGetScriptName()) gScriptsShown += [s];
}
list b = ["None", "Back"];
string t = "Step 5 of 5 (optional): which script flies her? It is stopped when she is shot down and started again when she respawns.\n";
for (i = 0; i < llGetListLength(gScriptsShown); ++i) { t += "\n" + (string)(i + 1) + ". " + llList2String(gScriptsShown, i); b += [(string)(i + 1)]; }
Menu("script", t, b);
}
SetupDone() {
llLinksetDataWrite("wolfair_ready", "1");
gStep = "";
if (gMenuL) { llListenRemove(gMenuL); gMenuL = 0; }
FindGunLinks();
string more = "";
if (gAxis == "auto") more = " First fly her straight for a few seconds so I can learn which way her nose points.";
Tell(gOwner, "Your aircraft is ready! Say \"dogfight on\" (or press DOGFIGHT on the flight deck) to fight." + more + " " + FireHelp());
}
SetupAnswer(string m) {
if (m == "Cancel") { gStep = ""; UnshowGuns(); return; }
if (gStep == "pilot") {
if (m == "Skip") { SetupNose(); return; }
integer n = llGetObjectPrimCount(llGetKey());
integer l;
integer found = -1;
for (l = 1; l <= n; ++l) if (llAvatarOnLinkSitTarget(l) == gOwner) found = l;
if (found == -1 && Seated(gOwner)) found = 0; // seated, but not on a sit target: "the first to sit"
if (found == -1) { Menu("pilot", "You are not sitting on her yet. Sit in the pilot's seat, then press \"Pilot seat\".", ["Pilot seat", "Skip", "Cancel"]); return; }
gPilotLink = found;
llLinksetDataWrite("wolfair_pilot", (string)found);
SetupNose();
return;
}
if (gStep == "nose") {
if (m == "Back") { SetupStart(); return; }
integer i = llListFindList(["Auto", "+X", "-X", "+Y", "-Y"], [m]);
if (i == -1) return;
if (i == 0) gAxis = "auto"; else gAxis = (string)(i - 1);
llLinksetDataWrite("wolfair_axis", gAxis);
SetupGuns();
return;
}
if (gStep == "guns") {
if (m == "Back") { SetupNose(); return; }
string g;
if (m == "Gun prims") {
g = GunPrims();
if (g == "") { Menu("guns", "She has no prims named gun... - pick one of the others.", ["2 in the nose", "4 in the wings", "Nose cannon", "Back"]); return; }
}
else {
if (gAxis == "auto") { Menu("guns", "The places in the nose or the wings need her nose direction: go Back and pick it (or use her gun prims).", ["Gun prims", "Back"]); return; }
if (m == "2 in the nose") g = Preset("nose2");
else if (m == "4 in the wings") g = Preset("wing4");
else g = Preset("cannon");
}
SetupGunsCheck(g);
return;
}
if (gStep == "gunscheck") {
UnshowGuns();
if (m == "Back") { SetupGuns(); return; }
llLinksetDataWrite("wolfair_guns", gGuns);
SetupFireKey();
return;
}
if (gStep == "firekey") {
if (m == "Back") { SetupGuns(); return; }
if (m == "Mouse (mouselook)") gFireKey = "ml";
else if (m == "Shift+arrows") gFireKey = "shift";
else gFireKey = "chat";
llLinksetDataWrite("wolfair_firekey", gFireKey);
SetupScript();
return;
}
if (gStep == "script") {
if (m == "Back") { SetupFireKey(); return; }
if (m == "None") gScript = "";
else gScript = llList2String(gScriptsShown, (integer)m - 1);
llLinksetDataWrite("wolfair_script", gScript);
SetupDone();
}
}
// ─────────────────────────────────────────────────────────────── orders
Order(key a, string m) {
m = llToLower(llStringTrim(m, STRING_TRIM));
integer mine = (a == gOwner || a == gPilot);
if (m == "hud hello 1" || m == "hud hello") {
if (!Seated(a)) return;
if (llListFindList(gHud, [a]) == -1 && llGetListLength(gHud) < 12) gHud += [a];
Hi(a);
Publish();
return;
}
if (m == "hud bye") { integer i = llListFindList(gHud, [a]); if (i != -1) gHud = llDeleteSubList(gHud, i, i); return; }
if (!mine) return;
if (m == "dogfight on" || m == "hud dogfight on") Dogfight(a, TRUE, FALSE);
else if (m == "dogfight off" || m == "hud dogfight off") Dogfight(a, FALSE, FALSE);
else if (m == "hud fire on" && a == gPilot) Trigger(TRUE);
else if (m == "hud fire off" && a == gPilot) Trigger(FALSE);
else if (m == "fire" && a == gPilot) { gTriggerUntil = llGetTime() + 1.0; Tick(); }
else if ((m == "bail" || m == "bail out" || m == "hud bail") && a == gPilot) Bail();
else if (m == "guns" || m == "cannon" || m == "all guns") {
gGroup = llList2String(["guns", "cannon", "all"], llListFindList(["guns", "cannon", "all guns"], [m]));
llLinksetDataWrite("wolfair_group", gGroup);
Tell(a, "Firing: " + gGroup + ".");
}
else if (m == "home here" && a == gOwner) { SetHome(); Tell(a, "She respawns here now."); }
else if (m == "setup" && a == gOwner) SetupStart();
else if (m == "status") Tell(a, Status());
}
string Status() {
string s = llGetObjectName() + " (Wolf Dogfight " + VER + "): dogfight mode " + gState;
if (!Ready()) return s + ". Not set up yet: touch her (owner).";
if (gState != "off") s += ". Damage: " + llDumpList2String(llParseString2List(gHealth, [","], []), " ")
+ ". Guns " + Field(gGunsSt, 0) + " rounds, " + Field(gGunsSt, 1) + "% hot; cannon " + Field(gCannonSt, 0) + " rounds.";
if (gSafe) s += " In safe airspace.";
return s;
}
Listens() {
if (gChL) llListenRemove(gChL);
if (gHelloL) llListenRemove(gHelloL);
if (gZeroL) llListenRemove(gZeroL);
gHelloL = llListen(HELLO_CH, "", NULL_KEY, "");
gChL = 0;
if (gCh != HELLO_CH) gChL = llListen(gCh, "", NULL_KEY, "");
gZeroL = llListen(0, "", gOwner, "");
}
Init() {
gOwner = llGetOwner();
gHelm = IsHelmHere();
gMenuCh = -1 - (integer)llFrand(1.0e9);
LoadSettings();
Listens();
gOn = FALSE;
gState = "off";
FindGunLinks();
Pilot();
if (llLinksetDataRead("wolfair_df") == "on" && Ready()) Dogfight(gOwner, TRUE, TRUE);
Tick();
}
default {
state_entry() { Init(); }
on_rez(integer p) { llResetScript(); }
changed(integer c) {
if (c & CHANGED_OWNER) { llLinksetDataWrite("wolfair_df", "off"); llLinksetDataDelete("wolfair_home"); llResetScript(); }
if (c & CHANGED_LINK) Pilot();
if (c & CHANGED_INVENTORY) gHelm = IsHelmHere();
if (c & CHANGED_REGION_START) { // a region restart ends every dogfight (DESIGN s3.10): she rejoins
gState = "off"; gSeq = -1; gEvSeq = -1;
if (llLinksetDataRead("wolfair_df") == "on" && Ready()) Dogfight(gOwner, TRUE, TRUE);
}
}
touch_start(integer n) {
key a = llDetectedKey(0);
if (a != gOwner) { Tell(a, Status()); return; }
if (!Ready()) SetupStart();
else Menu("main", Status(), ["Dogfight on", "Dogfight off", "Set up again", "Home here", "Close"]);
}
run_time_permissions(integer p) { if (p & PERMISSION_TAKE_CONTROLS) Grab(); }
control(key id, integer level, integer edge) {
if (id != gPilot) return;
integer c = CONTROL_ML_LBUTTON;
if (gFireKey == "shift") c = CONTROL_LEFT | CONTROL_RIGHT;
if (gFireKey == "chat") return;
Trigger((level & c) != 0 && gState == "on");
}
listen(integer ch, string name, key id, string m) {
if (ch == gMenuCh) {
if (gStep == "main") {
gStep = "";
if (m == "Dogfight on") Dogfight(id, TRUE, FALSE);
else if (m == "Dogfight off") Dogfight(id, FALSE, FALSE);
else if (m == "Set up again") SetupStart();
else if (m == "Home here") { SetHome(); Tell(id, "She respawns here now."); }
return;
}
SetupAnswer(m);
return;
}
if (llGetAgentSize(id) == ZERO_VECTOR) return; // people only
Order(id, m);
}
link_message(integer from, integer num, string s, key k) {
if (num != 7300) return;
list p = llParseString2List(s, ["|"], []);
string w = llList2String(p, 0);
if (w == "fire") Trigger((integer)llList2String(p, 1) && gState == "on");
else if (w == "bail") Bail();
else if (w == "crash") {
if (gState == "off") return;
list r = wolfCall("air.crash", [(float)llList2String(p, 1)]);
if (r != []) Absorb(llDumpList2String(["-1"] + llList2List(r, 1, -1), "|"));
}
}
object_rez(key id) {
if (llKey2Name(id) == "Wolf Parachute") BailWith((string)id);
}
dataserver(key q, string data) {
if (q == gWatchQ) {
gWatchQ = NULL_KEY;
if (llGetSubString(data, 0, 5) == "error:") { gWatchRetry = llGetTime() + 5.0; return; }
Absorb(data);
Watch();
return;
}
}
timer() {
float now = llGetTime();
if (gFalling) FallStep();
if (gAxis == "auto" && gClass == "" && gPilot != NULL_KEY) Learn();
if (gState == "on" && (gTrigger || now < gTriggerUntil)) Burst();
else Tracers(FALSE);
// the watch: lost, refused or stale (an idle watch is answered within 240 s, AirRules.WATCH_HEARTBEAT_S)
if ((gOn || gState != "off") && gWatchQ == NULL_KEY && now >= gWatchRetry) Watch();
if (gWatchQ != NULL_KEY && now - gWatchAt > 300.0) Watch();
Tick();
}
}
A scoreboard: the grid's top aces and this region's latest kills as floating text (wolf_air_scoreboard.lsl, 65 lines: show or hide)
Download wolf_air_scoreboard.lsl
// Wolf Air scoreboard - the grid's top aces as floating text, and this region's latest kills (wolf-air, 2026-11-01).
// Put it in any prim on a region with the wolf-air plugin. Touch it to refresh the aces.
// air.leaderboard answers "board|n" then one "rank|name|kills|deaths" line an ace (AirEngine.cs BoardText).
// air.feed answers "<seq>" then one "seq|unix|killer|killer plane|victim|victim plane|how" line a kill (AirEngine.cs FeedData).
key gBoardQ = NULL_KEY;
key gFeedQ = NULL_KEY;
integer gFeedSeq = -1;
string gAces = "Asking for the aces...";
list gKills; // the last 5 kills seen here
Show() {
string t = "WOLF AIR - TOP ACES\n" + gAces;
if (gKills != []) t += "\n\nLATEST HERE\n" + llDumpList2String(gKills, "\n");
llSetText(t, <1.0, 0.85, 0.4>, 1.0);
}
AskBoard() {
gBoardQ = wolfCallAsync("air.leaderboard", [5]);
if (gBoardQ == NULL_KEY) { gAces = "No dogfights here: " + wolfGetLastError(); Show(); }
}
AskFeed() {
gFeedQ = wolfCallAsync("air.feed", [gFeedSeq]); // answered when someone is shot down here
}
default {
state_entry() { AskBoard(); AskFeed(); llSetTimerEvent(600.0); Show(); }
on_rez(integer p) { llResetScript(); }
touch_start(integer n) { AskBoard(); }
timer() { AskBoard(); if (gFeedQ == NULL_KEY) AskFeed(); }
dataserver(key q, string data) {
if (q == gBoardQ) {
gBoardQ = NULL_KEY;
if (llGetSubString(data, 0, 5) == "error:") { gAces = llGetSubString(data, 6, -1); Show(); return; }
list lines = llParseString2List(data, ["\n"], []);
gAces = "";
integer i;
for (i = 1; i < llGetListLength(lines); ++i) {
list f = llParseStringKeepNulls(llList2String(lines, i), ["|"], []);
gAces += llList2String(f, 0) + ". " + llList2String(f, 1) + " - " + llList2String(f, 2) + " kills\n";
}
if (gAces == "") gAces = "Nobody has shot anyone down yet.";
Show();
}
else if (q == gFeedQ) {
gFeedQ = NULL_KEY;
if (llGetSubString(data, 0, 5) == "error:") return;
list lines = llParseString2List(data, ["\n"], []);
integer i;
for (i = 1; i < llGetListLength(lines); ++i) {
list f = llParseStringKeepNulls(llList2String(lines, i), ["|"], []);
if ((integer)llList2String(f, 0) > gFeedSeq || gFeedSeq == -1) {
string who = llList2String(f, 2);
if (who == "") who = "(nobody)";
gKills += [who + " downed " + llList2String(f, 4) + " (" + llList2String(f, 6) + ")"];
}
}
if (llGetListLength(gKills) > 5) gKills = llList2List(gKills, -5, -1);
gFeedSeq = (integer)llList2String(lines, 0);
Show();
AskFeed();
}
}
}
Ours alone. Grid features you will not find on any other OpenSimulator grid.