Merge edge into feat/title-conditions: §3's zones and domes
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 6s
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 6s
One conflict, ExpectedHooks: both phases' hooks are kept (48, all void). Compiled and loaded on both rigs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -29,8 +29,8 @@
|
|||||||
# exact commit being released.
|
# exact commit being released.
|
||||||
#
|
#
|
||||||
# 2. A FRAMEWORK-NEUTRAL LAYOUT. The tarball holds one directory,
|
# 2. A FRAMEWORK-NEUTRAL LAYOUT. The tarball holds one directory,
|
||||||
# `runicgateway-rust-plugin/`, with `RunicGateway.cs`, `RunicGatewayZones.cs`
|
# `runicgateway-rust-plugin/`, with `RunicGateway.cs`, its helpers
|
||||||
# and `manifest.json` at its top. Every `.cs` in it is listed in the
|
# (`RunicGatewayZones.cs`, `RunicGatewayDomes.cs`) and `manifest.json` at its top. Every `.cs` in it is listed in the
|
||||||
# manifest's `files`, and the installer and the egg place every one of them. It does NOT mirror `oxide/plugins/`: the same file goes to
|
# manifest's `files`, and the installer and the egg place every one of them. It does NOT mirror `oxide/plugins/`: the same file goes to
|
||||||
# `oxide/plugins/` or `carbon/plugins/`, and the installer and the egg each
|
# `oxide/plugins/` or `carbon/plugins/`, and the installer and the egg each
|
||||||
# decide which (§34.2.1). The fixed, unversioned prefix is so a reader never
|
# decide which (§34.2.1). The fixed, unversioned prefix is so a reader never
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
# `oxide.plugins` / `c.plugins` on a server names the release it runs. The
|
# `oxide.plugins` / `c.plugins` on a server names the release it runs. The
|
||||||
# committed file keeps its placeholder; the manifest records the commit.
|
# committed file keeps its placeholder; the manifest records the commit.
|
||||||
# Only the bridge is stamped. A helper (docs/modules/rust/PLAN_FIXES.md D168,
|
# Only the bridge is stamped. A helper (docs/modules/rust/PLAN_FIXES.md D168,
|
||||||
# D182 — today `RunicGatewayZones.cs`, the ZoneManager helper) keeps its own
|
# D182 — today `RunicGatewayZones.cs` for ZoneManager and `RunicGatewayDomes.cs`
|
||||||
|
# for ZoneDomes, PLAN_REDESIGNS D194) keeps its own
|
||||||
# version: it does one job, changes rarely, and the bridge reads its version at
|
# version: it does one job, changes rarely, and the bridge reads its version at
|
||||||
# hello to know which job it does.
|
# hello to know which job it does.
|
||||||
#
|
#
|
||||||
@@ -80,7 +81,7 @@ env:
|
|||||||
PLUGIN: overlay/oxide/plugins/RunicGateway.cs
|
PLUGIN: overlay/oxide/plugins/RunicGateway.cs
|
||||||
# The helpers shipped beside the bridge (D168, D182). Optional at runtime —
|
# The helpers shipped beside the bridge (D168, D182). Optional at runtime —
|
||||||
# the bridge loads without them — and installed by default.
|
# the bridge loads without them — and installed by default.
|
||||||
HELPERS: overlay/oxide/plugins/RunicGatewayZones.cs
|
HELPERS: overlay/oxide/plugins/RunicGatewayZones.cs overlay/oxide/plugins/RunicGatewayDomes.cs
|
||||||
SEED_VERSION: "0.1.0"
|
SEED_VERSION: "0.1.0"
|
||||||
INSTALLER_REPO: RunicGateway/installer
|
INSTALLER_REPO: RunicGateway/installer
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ unauthenticated command channel on the network.
|
|||||||
```
|
```
|
||||||
overlay/oxide/plugins/RunicGateway.cs → <server>/oxide/plugins/RunicGateway.cs
|
overlay/oxide/plugins/RunicGateway.cs → <server>/oxide/plugins/RunicGateway.cs
|
||||||
overlay/oxide/plugins/RunicGatewayZones.cs → <server>/oxide/plugins/RunicGatewayZones.cs
|
overlay/oxide/plugins/RunicGatewayZones.cs → <server>/oxide/plugins/RunicGatewayZones.cs
|
||||||
|
overlay/oxide/plugins/RunicGatewayDomes.cs → <server>/oxide/plugins/RunicGatewayDomes.cs
|
||||||
```
|
```
|
||||||
|
|
||||||
`RunicGatewayZones.cs` is the **ZoneManager helper** (docs/modules/rust/PLAN_FIXES.md D181, D182). ZoneManager
|
`RunicGatewayZones.cs` is the **ZoneManager helper** (docs/modules/rust/PLAN_FIXES.md D181, D182). ZoneManager
|
||||||
@@ -44,6 +45,13 @@ somebody already standing there holds nobody. The helper patches that with Harmo
|
|||||||
bridge still loads, scores its own zones by position, and reports the helper missing at hello. `rgz.status` in the
|
bridge still loads, scores its own zones by position, and reports the helper missing at hello. `rgz.status` in the
|
||||||
server console says whether it patched.
|
server console says whether it patched.
|
||||||
|
|
||||||
|
`RunicGatewayDomes.cs` is the **ZoneDomes helper** (docs/modules/rust/PLAN_REDESIGNS.md §3.2, D194). ZoneDomes'
|
||||||
|
API ends by telling the player who asked how it went, and throws when a plugin calls it with no player; and at its
|
||||||
|
own start it drops the dome of every zone that does not exist yet, which is every zone the bridge re-creates. The
|
||||||
|
helper patches the first with Harmony and tells the bridge when ZoneDomes has started, so the bridge can put its
|
||||||
|
domes back without drawing any twice. It is optional: without it the site offers no dome, and says which piece is
|
||||||
|
missing. `rgd.status` in the server console says whether it patched.
|
||||||
|
|
||||||
Oxide compiles and loads it on the write, and writes `oxide/config/RunicGateway.json` on first load:
|
Oxide compiles and loads it on the write, and writes `oxide/config/RunicGateway.json` on first load:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -247,7 +247,8 @@ namespace Oxide.Plugins
|
|||||||
"OnEntitySpawned", "OnEntityKill",
|
"OnEntitySpawned", "OnEntityKill",
|
||||||
"OnEntityTakeDamage", "OnItemCraftFinished", "OnEntityBuilt", "OnStructureRepair",
|
"OnEntityTakeDamage", "OnItemCraftFinished", "OnEntityBuilt", "OnStructureRepair",
|
||||||
"OnHealingItemUse", "OnPlayerRevive", "OnRocketLaunched",
|
"OnHealingItemUse", "OnPlayerRevive", "OnRocketLaunched",
|
||||||
"OnExplosiveThrown", "OnExplosiveDropped", "OnMissionSucceeded"
|
"OnExplosiveThrown", "OnExplosiveDropped", "OnMissionSucceeded",
|
||||||
|
"OnEnterZone", "OnExitZone", "OnRgDomesReady"
|
||||||
};
|
};
|
||||||
|
|
||||||
private void Init()
|
private void Init()
|
||||||
@@ -1176,6 +1177,17 @@ namespace Oxide.Plugins
|
|||||||
}
|
}
|
||||||
|
|
||||||
hello["zoneHelper"] = helper;
|
hello["zoneHelper"] = helper;
|
||||||
|
|
||||||
|
// D211: ZoneManager's own flag list, for the site's zone presets (§3.1). In hello
|
||||||
|
// rather than every status frame, because it changes only when ZoneManager does,
|
||||||
|
// and a ZoneManager load sends hello again.
|
||||||
|
var zoneManager = new Dictionary<string, object> { { "version", ZoneManagerPlugin().Version.ToString() } };
|
||||||
|
List<string> flags = ZoneFlagNames(ZoneManagerPlugin());
|
||||||
|
|
||||||
|
if (flags != null)
|
||||||
|
zoneManager["flags"] = flags;
|
||||||
|
|
||||||
|
hello["zoneManager"] = zoneManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
EmitFrame(hello);
|
EmitFrame(hello);
|
||||||
@@ -4910,6 +4922,22 @@ namespace Oxide.Plugins
|
|||||||
EmitFrame(frame);
|
EmitFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsZonePlugin(Plugin plugin)
|
||||||
|
{
|
||||||
|
if (plugin == null || plugin == this)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string name = plugin.Name;
|
||||||
|
return name == "ZoneManager" || name == "ZoneDomes" || name == "RunicGatewayZones" || name == "RunicGatewayDomes";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main thread. Hello again, once the world is up and the link is.</summary>
|
||||||
|
private void ResendHello()
|
||||||
|
{
|
||||||
|
if (_worldReady && _connected)
|
||||||
|
SendHello();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Main thread. Queues <see cref="EmitPluginChange"/> for somebody else's plugin, once the world is up.</summary>
|
/// <summary>Main thread. Queues <see cref="EmitPluginChange"/> for somebody else's plugin, once the world is up.</summary>
|
||||||
private void NotePluginChange(Plugin plugin, bool loaded)
|
private void NotePluginChange(Plugin plugin, bool loaded)
|
||||||
{
|
{
|
||||||
@@ -4931,6 +4959,11 @@ namespace Oxide.Plugins
|
|||||||
if (plugin != null && plugin != this && plugin.Name == "ZoneManager")
|
if (plugin != null && plugin != this && plugin.Name == "ZoneManager")
|
||||||
NextTick(() => RecreateZones("ZoneManager reloaded"));
|
NextTick(() => RecreateZones("ZoneManager reloaded"));
|
||||||
|
|
||||||
|
// Hello carries ZoneManager's flag list and both helpers' states, so a change to any
|
||||||
|
// of the four is said again rather than waiting for the next connect (§3.1, §3.2).
|
||||||
|
if (IsZonePlugin(plugin))
|
||||||
|
NextTick(ResendHello);
|
||||||
|
|
||||||
// BetterChat keeps third-party titles on its INSTANCE, so a reloaded BetterChat has
|
// BetterChat keeps third-party titles on its INSTANCE, so a reloaded BetterChat has
|
||||||
// forgotten our getter. Registering again is the only way back (§33.1).
|
// forgotten our getter. Registering again is the only way back (§33.1).
|
||||||
if (plugin != null && plugin != this && plugin.Name == "BetterChat")
|
if (plugin != null && plugin != this && plugin.Name == "BetterChat")
|
||||||
@@ -4953,6 +4986,9 @@ namespace Oxide.Plugins
|
|||||||
if (plugin != null && plugin.Name == "BetterChat")
|
if (plugin != null && plugin.Name == "BetterChat")
|
||||||
_titlesRegistered = false;
|
_titlesRegistered = false;
|
||||||
|
|
||||||
|
if (IsZonePlugin(plugin))
|
||||||
|
NextTick(ResendHello);
|
||||||
|
|
||||||
ConfigWriteJob job = _configJob;
|
ConfigWriteJob job = _configJob;
|
||||||
|
|
||||||
if (job == null || plugin == null)
|
if (job == null || plugin == null)
|
||||||
@@ -7829,6 +7865,24 @@ namespace Oxide.Plugins
|
|||||||
public float Radius;
|
public float Radius;
|
||||||
public string Name;
|
public string Name;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A zone's ZoneManager flags and settings (PLAN_REDESIGNS §3.1), kept so a re-created
|
||||||
|
/// zone gets them back. Null on a zone from before protocol 13's options.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Flags;
|
||||||
|
public Dictionary<string, string> Settings;
|
||||||
|
|
||||||
|
/// <summary>What a player is told on the way in and out, and how (D193). Said by this plugin, not ZoneManager.</summary>
|
||||||
|
public string EnterMessage;
|
||||||
|
public string LeaveMessage;
|
||||||
|
public string Delivery;
|
||||||
|
public string Format;
|
||||||
|
|
||||||
|
/// <summary>A ZoneDomes dome over the zone (D194): its type (0–4) and how many spheres are stacked.</summary>
|
||||||
|
public bool Dome;
|
||||||
|
public int DomeType;
|
||||||
|
public int DomeStack;
|
||||||
|
|
||||||
/// <summary>When a zone is erased without being asked, on this host's clock (D96). Zero for an entity.</summary>
|
/// <summary>When a zone is erased without being asked, on this host's clock (D96). Zero for an entity.</summary>
|
||||||
public long DeadlineMs;
|
public long DeadlineMs;
|
||||||
|
|
||||||
@@ -7925,6 +7979,401 @@ namespace Oxide.Plugins
|
|||||||
return inside is bool && (bool)inside;
|
return inside is bool && (bool)inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- a zone's options (PLAN_REDESIGNS §3.1, D166, D211) ----
|
||||||
|
//
|
||||||
|
// ZoneManager's flags are whatever the installed version declares, so the list is read from
|
||||||
|
// it, never kept here. `ZoneFieldListRaw` is in ZoneManager's own API region and answers
|
||||||
|
// the zone fields followed by every flag name; the fields this plugin controls or does not
|
||||||
|
// offer are dropped from the front. Plan §0.6 believed only a plugin that references
|
||||||
|
// ZoneManager's types could read the list, so it put the read in the zone helper. That was
|
||||||
|
// wrong for 3.1.14 (D211), and a server without the helper still gets every flag.
|
||||||
|
|
||||||
|
/// <summary>The fields `ZoneFieldListRaw` answers that are not flags, lower-cased.</summary>
|
||||||
|
private static readonly HashSet<string> ZoneNonFlagFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"name", "id", "radius", "rotation", "size", "location", "enter_message", "leave_message",
|
||||||
|
"radiation", "comfort", "temperature", "safezone", "permission", "ejectspawns", "parentid", "enabled", "enable"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The settings a step may give a zone, and their bounds. Every other ZoneManager field
|
||||||
|
/// (rotation, parent, eject spawns, size) changes what this plugin believes the zone is,
|
||||||
|
/// so it stays out (§3.1). The messages are this plugin's own (D193).
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string[] ZoneSettingKeys = { "radiation", "comfort", "temperature", "safezone", "permission" };
|
||||||
|
|
||||||
|
private const float ZoneRadiationMax = 500f;
|
||||||
|
private const float ZoneComfortMax = 1f;
|
||||||
|
private const float ZoneTemperatureLimit = 100f;
|
||||||
|
private const int ZoneMessageMax = 256;
|
||||||
|
private const int ZoneFlagsMax = 80;
|
||||||
|
|
||||||
|
/// <summary>ZoneDomes' own sphere types: Standard, Red, Blue, Green, Purple.</summary>
|
||||||
|
private const int DomeTypeMax = 4;
|
||||||
|
|
||||||
|
/// <summary>The most spheres one dome stacks. Each is an entity; ZoneDomes itself has no limit.</summary>
|
||||||
|
private const int DomeStackMax = 10;
|
||||||
|
|
||||||
|
/// <summary>A zone this plugin just put back says nothing to whoever is inside it for this long.</summary>
|
||||||
|
private const long ZoneQuietMs = 5000L;
|
||||||
|
|
||||||
|
/// <summary>Zone id → the moment its enter and leave messages start again. Main thread.</summary>
|
||||||
|
private readonly Dictionary<string, long> _zoneQuietUntil = new Dictionary<string, long>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main thread. ZoneManager's flag names, as it spells them, or null when it is not loaded
|
||||||
|
/// or does not answer. Asked when needed: an operator can update ZoneManager at any time.
|
||||||
|
/// </summary>
|
||||||
|
private static List<string> ZoneFlagNames(Plugin zm)
|
||||||
|
{
|
||||||
|
var fields = zm == null ? null : zm.Call("ZoneFieldListRaw") as List<string>;
|
||||||
|
|
||||||
|
if (fields == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var flags = new List<string>();
|
||||||
|
|
||||||
|
foreach (string field in fields)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(field) && !ZoneNonFlagFields.Contains(field))
|
||||||
|
flags.Add(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main thread. Reads a step's flags and settings against the live ZoneManager. Each flag
|
||||||
|
/// is returned as ZoneManager spells it; each setting is checked and written as
|
||||||
|
/// ZoneManager parses it. False with a refusal on anything unknown or out of range.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ReadZoneOptions(Plugin zm, Dictionary<string, object> cmd,
|
||||||
|
out List<string> flags, out Dictionary<string, string> settings, out string refusal)
|
||||||
|
{
|
||||||
|
flags = new List<string>();
|
||||||
|
settings = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
refusal = null;
|
||||||
|
|
||||||
|
object raw;
|
||||||
|
|
||||||
|
if (cmd.TryGetValue("flags", out raw) && raw != null)
|
||||||
|
{
|
||||||
|
var list = raw as Newtonsoft.Json.Linq.JArray;
|
||||||
|
|
||||||
|
if (list == null)
|
||||||
|
{
|
||||||
|
refusal = "a zone's flags are a list of names";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.Count > ZoneFlagsMax)
|
||||||
|
{
|
||||||
|
refusal = $"a zone takes at most {ZoneFlagsMax} flags, not {list.Count}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> known = ZoneFlagNames(zm);
|
||||||
|
|
||||||
|
if (known == null)
|
||||||
|
{
|
||||||
|
refusal = "this server's ZoneManager did not say which flags it has";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var byName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (string k in known)
|
||||||
|
byName[k] = k;
|
||||||
|
|
||||||
|
foreach (Newtonsoft.Json.Linq.JToken token in list)
|
||||||
|
{
|
||||||
|
string name = token.Type == Newtonsoft.Json.Linq.JTokenType.String ? (string)token : null;
|
||||||
|
string spelled;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(name) || !byName.TryGetValue(name.Trim(), out spelled))
|
||||||
|
{
|
||||||
|
refusal = $"'{name}' is not a flag this server's ZoneManager has";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.Contains(spelled))
|
||||||
|
flags.Add(spelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.TryGetValue("settings", out raw) && raw != null)
|
||||||
|
{
|
||||||
|
var obj = raw as Newtonsoft.Json.Linq.JObject;
|
||||||
|
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
refusal = "a zone's settings are an object of name and value";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Newtonsoft.Json.Linq.JProperty p in obj.Properties())
|
||||||
|
{
|
||||||
|
string key = p.Name.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (Array.IndexOf(ZoneSettingKeys, key) < 0)
|
||||||
|
{
|
||||||
|
refusal = $"'{p.Name}' is not a zone setting this server offers (radiation, comfort, temperature, safezone, permission)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant, because a JSON 0.5 read under the host's culture can come back "0,5".
|
||||||
|
var jv = p.Value as Newtonsoft.Json.Linq.JValue;
|
||||||
|
string value = jv == null ? null : Convert.ToString(jv.Value, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
string written;
|
||||||
|
|
||||||
|
if (!ZoneSettingValue(key, value, out written, out refusal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
settings[key] = written;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ZoneSettingValue(string key, string value, out string written, out string refusal)
|
||||||
|
{
|
||||||
|
written = null;
|
||||||
|
refusal = null;
|
||||||
|
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
if (key == "safezone")
|
||||||
|
{
|
||||||
|
bool on;
|
||||||
|
|
||||||
|
if (!bool.TryParse(value, out on))
|
||||||
|
{
|
||||||
|
refusal = $"safezone is true or false, not '{value}'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
written = on ? "true" : "false";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == "permission")
|
||||||
|
{
|
||||||
|
string perm = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
if (perm.StartsWith("zonemanager.", StringComparison.Ordinal))
|
||||||
|
perm = perm.Substring("zonemanager.".Length);
|
||||||
|
|
||||||
|
if (perm.Length == 0 || perm.Length > 32)
|
||||||
|
{
|
||||||
|
refusal = "a zone's permission is 1 to 32 characters";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (char c in perm)
|
||||||
|
{
|
||||||
|
if (!(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_')
|
||||||
|
{
|
||||||
|
refusal = $"a zone's permission is letters, digits and _, not '{value}'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
written = perm;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float number;
|
||||||
|
|
||||||
|
if (!float.TryParse(value, System.Globalization.NumberStyles.Float, inv, out number) || float.IsNaN(number) || float.IsInfinity(number))
|
||||||
|
{
|
||||||
|
refusal = $"{key} is a number, not '{value}'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float low = key == "temperature" ? -ZoneTemperatureLimit : 0f;
|
||||||
|
float high = key == "radiation" ? ZoneRadiationMax : key == "comfort" ? ZoneComfortMax : ZoneTemperatureLimit;
|
||||||
|
|
||||||
|
if (number < low || number > high)
|
||||||
|
{
|
||||||
|
refusal = $"{key} is {low} to {high}, not {number.ToString(inv)}";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
written = number.ToString(inv);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the dome (PLAN_REDESIGNS §3.2, D167, D194) ----
|
||||||
|
//
|
||||||
|
// ZoneDomes draws the dome. Two things it lacks are made up here and in the domes helper,
|
||||||
|
// `RunicGatewayDomes.cs`: its API ends in a chat message to the player who asked, which
|
||||||
|
// throws for a call with none (the helper patches that), and at its own start it drops
|
||||||
|
// every dome whose zone does not exist yet, which is every zone this plugin re-creates.
|
||||||
|
// So this plugin puts its domes back after it puts its zones back, and only once ZoneDomes
|
||||||
|
// has finished its own start: a dome added before that is drawn twice, and the first set
|
||||||
|
// of spheres is left in the world where nothing can remove it. The helper says when.
|
||||||
|
|
||||||
|
private Plugin ZoneDomesPlugin()
|
||||||
|
{
|
||||||
|
return plugins.Find("ZoneDomes");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Plugin DomesHelperPlugin()
|
||||||
|
{
|
||||||
|
return plugins.Find("RunicGatewayDomes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main thread. <c>patched</c>, <c>unsupported</c>, <c>no-zonedomes</c> or <c>missing</c>.</summary>
|
||||||
|
private string DomesHelperState()
|
||||||
|
{
|
||||||
|
Plugin helper = DomesHelperPlugin();
|
||||||
|
return helper == null ? "missing" : helper.Call("State") as string ?? "unsupported";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main thread. Whether a dome may be added now: ZoneDomes loaded, patched, and past its own start.</summary>
|
||||||
|
private bool DomesReady()
|
||||||
|
{
|
||||||
|
Plugin helper = DomesHelperPlugin();
|
||||||
|
return ZoneDomesPlugin() != null && helper != null
|
||||||
|
&& helper.Call("State") as string == "patched" && helper.Call("Ready") is bool ready && ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main thread. Why a dome cannot be added now, or null when it can.</summary>
|
||||||
|
private string DomeRefusal()
|
||||||
|
{
|
||||||
|
if (ZoneDomesPlugin() == null)
|
||||||
|
return "ZoneDomes is not loaded on this server, so a zone cannot have a dome";
|
||||||
|
|
||||||
|
string state = DomesHelperState();
|
||||||
|
|
||||||
|
if (state == "missing")
|
||||||
|
return "RunicGatewayDomes.cs is not installed beside the bridge, and ZoneDomes cannot be called without it";
|
||||||
|
|
||||||
|
if (state != "patched")
|
||||||
|
return $"RunicGatewayDomes.cs could not patch this ZoneDomes ({state})";
|
||||||
|
|
||||||
|
return DomesReady() ? null : "ZoneDomes has not finished starting";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Main thread. Adds a zone's dome. True when ZoneDomes made it, or already had it.</summary>
|
||||||
|
private bool AddDome(WorldEntry e)
|
||||||
|
{
|
||||||
|
Plugin zd = ZoneDomesPlugin();
|
||||||
|
|
||||||
|
if (zd == null || !e.Dome)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
object made = zd.Call("AddNewDome", null, e.Id, e.DomeType, e.DomeStack);
|
||||||
|
return made is bool && (bool)made;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main thread. Takes a zone's dome out of the world and out of ZoneDomes' data file.
|
||||||
|
/// Called BEFORE the zone is erased, so a dome never outlives its zone.
|
||||||
|
/// </summary>
|
||||||
|
private void RemoveDome(WorldEntry e)
|
||||||
|
{
|
||||||
|
Plugin zd = ZoneDomesPlugin();
|
||||||
|
|
||||||
|
if (zd == null || !e.Dome)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Without the helper the call still removes the dome and then throws on its message;
|
||||||
|
// the framework logs that, and the dome is gone either way.
|
||||||
|
zd.Call("RemoveExistingDome", null, e.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main thread. Puts back the dome of every standing zone that has one. ZoneDomes answers
|
||||||
|
/// false for a dome it already has, so this is safe to repeat.
|
||||||
|
/// </summary>
|
||||||
|
private void RestoreDomes(string why)
|
||||||
|
{
|
||||||
|
if (!_worldReady || !DomesReady())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Plugin zm = ZoneManagerPlugin();
|
||||||
|
|
||||||
|
if (zm == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
|
||||||
|
foreach (WorldEntry e in _world.Values)
|
||||||
|
{
|
||||||
|
if (e.Kind != WorldKindZone || !e.Dome || ZoneExpired(e) || zm.Call("CheckZoneID", e.Id) == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (AddDome(e))
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added > 0)
|
||||||
|
{
|
||||||
|
_lastWorldSummary = $"domes after {why}: {added} put back";
|
||||||
|
Puts($"world: {_lastWorldSummary}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Raised by the domes helper when ZoneDomes has finished its own start.</summary>
|
||||||
|
private void OnRgDomesReady()
|
||||||
|
{
|
||||||
|
MarkHook("OnRgDomesReady");
|
||||||
|
NextTick(() => RestoreDomes("ZoneDomes started"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the zone's messages (D193) ----
|
||||||
|
//
|
||||||
|
// ZoneManager can say a zone's enter and leave messages itself, but only to chat unless its
|
||||||
|
// own config turns popups on for every zone on the server. So this plugin never gives it
|
||||||
|
// the messages for its own zones, and says them here, as each zone's step chose.
|
||||||
|
|
||||||
|
private void OnEnterZone(string zoneId, BasePlayer player)
|
||||||
|
{
|
||||||
|
MarkHook("OnEnterZone");
|
||||||
|
SayZoneMessage(zoneId, player, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExitZone(string zoneId, BasePlayer player)
|
||||||
|
{
|
||||||
|
MarkHook("OnExitZone");
|
||||||
|
SayZoneMessage(zoneId, player, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SayZoneMessage(string zoneId, BasePlayer player, bool entering)
|
||||||
|
{
|
||||||
|
WorldEntry e;
|
||||||
|
|
||||||
|
if (zoneId == null || player == null || !player.IsConnected || !_world.TryGetValue(zoneId, out e) || e.Kind != WorldKindZone)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string message = entering ? e.EnterMessage : e.LeaveMessage;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// A zone this plugin just put back (a restart, a ZoneManager reload) re-enters whoever
|
||||||
|
// is standing in it. They were told when they walked in.
|
||||||
|
long quiet;
|
||||||
|
|
||||||
|
if (_zoneQuietUntil.TryGetValue(zoneId, out quiet))
|
||||||
|
{
|
||||||
|
if (NowMs() < quiet)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_zoneQuietUntil.Remove(zoneId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A popup where PopupNotifications is loaded, and chat where it is not (§2.4): the
|
||||||
|
// step was accepted long before this moment, and a player is told either way.
|
||||||
|
Plugin popup = e.Delivery == DeliveryPopup ? PopupPlugin() : null;
|
||||||
|
|
||||||
|
if (popup != null)
|
||||||
|
popup.Call("CreatePopupNotification", message, player, 0f);
|
||||||
|
else
|
||||||
|
player.ChatMessage(e.Format == null ? message : Voice(e.Format, message));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The map's monuments in the one order every caller agrees on: grouped by prefab short
|
/// The map's monuments in the one order every caller agrees on: grouped by prefab short
|
||||||
/// name, then by position. <c>#n</c> in a value counts within its group, so it names the
|
/// name, then by position. <c>#n</c> in a value counts within its group, so it names the
|
||||||
@@ -8287,6 +8736,86 @@ namespace Oxide.Plugins
|
|||||||
if (name.Length > 64)
|
if (name.Length > 64)
|
||||||
name = name.Substring(0, 64);
|
name = name.Substring(0, 64);
|
||||||
|
|
||||||
|
// PLAN_REDESIGNS §3.1: the flags and settings, each checked against this server's own
|
||||||
|
// ZoneManager. An unknown one is refused, never dropped: a zone that quietly lacks the
|
||||||
|
// "no building" its author chose is worse than a step that fails and says why.
|
||||||
|
List<string> flags;
|
||||||
|
Dictionary<string, string> settings;
|
||||||
|
string optionRefusal;
|
||||||
|
|
||||||
|
if (!ReadZoneOptions(zm, cmd, out flags, out settings, out optionRefusal))
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "bad-option", optionRefusal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D193: what a player is told on the way in and out, and whether as chat or a popup.
|
||||||
|
string enterMessage = StringField(cmd, "enterMessage");
|
||||||
|
string leaveMessage = StringField(cmd, "leaveMessage");
|
||||||
|
|
||||||
|
if ((enterMessage != null && enterMessage.Length > ZoneMessageMax) || (leaveMessage != null && leaveMessage.Length > ZoneMessageMax))
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "out-of-range", $"a zone's message is at most {ZoneMessageMax} characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string delivery = StringField(cmd, "delivery");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(delivery))
|
||||||
|
delivery = DeliveryChat;
|
||||||
|
|
||||||
|
if (delivery != DeliveryChat && delivery != DeliveryPopup)
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "malformed", $"a zone's messages go to chat or to a popup, not to '{delivery}'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string format = StringField(cmd, "format");
|
||||||
|
|
||||||
|
if (format != null && (format.Length > ChatFormatMax || Occurrences(format, "{message}") != 1))
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "malformed", $"a chat format holds exactly one {{message}} and at most {ChatFormatMax} characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// D194: a dome, where ZoneDomes and the domes helper are both here.
|
||||||
|
bool dome = false;
|
||||||
|
int domeType = 0, domeStack = 1;
|
||||||
|
object domeRaw;
|
||||||
|
|
||||||
|
if (cmd.TryGetValue("dome", out domeRaw) && domeRaw != null)
|
||||||
|
{
|
||||||
|
var d = domeRaw as Newtonsoft.Json.Linq.JObject;
|
||||||
|
|
||||||
|
if (d == null)
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "malformed", "a zone's dome is an object of type and stack");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Newtonsoft.Json.Linq.JToken t = d["type"], s = d["stack"];
|
||||||
|
// No type is Standard, the one full dome; the colours show only where they meet
|
||||||
|
// the ground (D212).
|
||||||
|
domeType = t == null ? 0 : t.Type == Newtonsoft.Json.Linq.JTokenType.Integer ? (int)t : -1;
|
||||||
|
domeStack = s == null ? 1 : s.Type == Newtonsoft.Json.Linq.JTokenType.Integer ? (int)s : -1;
|
||||||
|
|
||||||
|
if (domeType < 0 || domeType > DomeTypeMax || domeStack < 1 || domeStack > DomeStackMax)
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "out-of-range", $"a dome's type is 0 to {DomeTypeMax} and its stack 1 to {DomeStackMax}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string noDome = DomeRefusal();
|
||||||
|
|
||||||
|
if (noDome != null)
|
||||||
|
{
|
||||||
|
EmitWorldError(reqId, "dome-unavailable", noDome);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dome = true;
|
||||||
|
}
|
||||||
|
|
||||||
var entry = new WorldEntry
|
var entry = new WorldEntry
|
||||||
{
|
{
|
||||||
Id = $"rg-{ShortRun(runId)}-{NowMs() % 100000000L}-{++_zoneSerial}",
|
Id = $"rg-{ShortRun(runId)}-{NowMs() % 100000000L}-{++_zoneSerial}",
|
||||||
@@ -8298,6 +8827,15 @@ namespace Oxide.Plugins
|
|||||||
Z = point.z,
|
Z = point.z,
|
||||||
Radius = (float)radius,
|
Radius = (float)radius,
|
||||||
Name = name,
|
Name = name,
|
||||||
|
Flags = flags.Count > 0 ? flags : null,
|
||||||
|
Settings = settings.Count > 0 ? settings : null,
|
||||||
|
EnterMessage = string.IsNullOrEmpty(enterMessage) ? null : enterMessage,
|
||||||
|
LeaveMessage = string.IsNullOrEmpty(leaveMessage) ? null : leaveMessage,
|
||||||
|
Delivery = delivery,
|
||||||
|
Format = delivery == DeliveryChat ? format : null,
|
||||||
|
Dome = dome,
|
||||||
|
DomeType = domeType,
|
||||||
|
DomeStack = domeStack,
|
||||||
DeadlineMs = NowMs() + holdMs,
|
DeadlineMs = NowMs() + holdMs,
|
||||||
BootId = _bootId,
|
BootId = _bootId,
|
||||||
WipeId = _wipeId,
|
WipeId = _wipeId,
|
||||||
@@ -8310,11 +8848,20 @@ namespace Oxide.Plugins
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The dome after the zone, because ZoneDomes reads the zone's place and size from
|
||||||
|
// ZoneManager. A dome that is not made takes its zone with it: the step asked for both.
|
||||||
|
if (dome && !AddDome(entry))
|
||||||
|
{
|
||||||
|
zm.Call("EraseTemporaryZone", this, entry.Id);
|
||||||
|
EmitWorldError(reqId, "dome-unavailable", "ZoneDomes did not make the dome, so the zone was not opened");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_world[entry.Id] = entry;
|
_world[entry.Id] = entry;
|
||||||
SaveWorld();
|
SaveWorld();
|
||||||
EnsureWorldTimer();
|
EnsureWorldTimer();
|
||||||
|
|
||||||
_lastWorldSummary = $"zone {entry.Id} r={radius:0} for {holdMs / 60000L} min, run {runId}";
|
_lastWorldSummary = $"zone {entry.Id} r={radius:0} for {holdMs / 60000L} min, {flags.Count} flag(s){(dome ? ", domed" : "")}, run {runId}";
|
||||||
Puts($"world: {_lastWorldSummary}");
|
Puts($"world: {_lastWorldSummary}");
|
||||||
EmitWorldPlaced(reqId, new List<WorldEntry> { entry }, false);
|
EmitWorldPlaced(reqId, new List<WorldEntry> { entry }, false);
|
||||||
}
|
}
|
||||||
@@ -8342,13 +8889,35 @@ namespace Oxide.Plugins
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool CreateZone(Plugin zm, WorldEntry entry)
|
private bool CreateZone(Plugin zm, WorldEntry entry)
|
||||||
{
|
{
|
||||||
string[] args =
|
var args = new List<string>
|
||||||
{
|
{
|
||||||
"name", entry.Name ?? "Event zone",
|
"name", entry.Name ?? "Event zone",
|
||||||
"radius", entry.Radius.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
"radius", entry.Radius.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
||||||
};
|
};
|
||||||
|
|
||||||
object created = zm.Call("CreateOrUpdateTemporaryZone", this, entry.Id, args, new UnityEngine.Vector3(entry.X, entry.Y, entry.Z));
|
// ZoneManager's own key/value arguments (§3.1): each setting by its key, and each flag
|
||||||
|
// as "<flag>", "true". Kept on the record, so a zone put back after a restart or a
|
||||||
|
// ZoneManager reload gets them back. A flag a newer ZoneManager no longer has is
|
||||||
|
// ignored by ZoneManager rather than failing the zone.
|
||||||
|
if (entry.Settings != null)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, string> kv in entry.Settings)
|
||||||
|
{
|
||||||
|
args.Add(kv.Key);
|
||||||
|
args.Add(kv.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Flags != null)
|
||||||
|
{
|
||||||
|
foreach (string flag in entry.Flags)
|
||||||
|
{
|
||||||
|
args.Add(flag);
|
||||||
|
args.Add("true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object created = zm.Call("CreateOrUpdateTemporaryZone", this, entry.Id, args.ToArray(), new UnityEngine.Vector3(entry.X, entry.Y, entry.Z));
|
||||||
|
|
||||||
return created is bool && (bool)created && zm.Call("CheckZoneID", entry.Id) != null;
|
return created is bool && (bool)created && zm.Call("CheckZoneID", entry.Id) != null;
|
||||||
}
|
}
|
||||||
@@ -8378,9 +8947,14 @@ namespace Oxide.Plugins
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (CreateZone(zm, e))
|
if (CreateZone(zm, e))
|
||||||
|
{
|
||||||
restored++;
|
restored++;
|
||||||
|
_zoneQuietUntil[e.Id] = NowMs() + ZoneQuietMs;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
failed++;
|
failed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restored > 0 || failed > 0)
|
if (restored > 0 || failed > 0)
|
||||||
@@ -8388,6 +8962,10 @@ namespace Oxide.Plugins
|
|||||||
_lastWorldSummary = $"zones after {why}: {restored} re-created, {failed} failed";
|
_lastWorldSummary = $"zones after {why}: {restored} re-created, {failed} failed";
|
||||||
Puts($"world: {_lastWorldSummary}");
|
Puts($"world: {_lastWorldSummary}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The domes after the zones (D194). At a real start ZoneDomes has not finished its own
|
||||||
|
// start yet, and the helper's ready hook brings this back when it has.
|
||||||
|
RestoreDomes(why);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Main thread. Place crates or NPCs an event owns, one registry entry per net id.</summary>
|
/// <summary>Main thread. Place crates or NPCs an event owns, one registry entry per net id.</summary>
|
||||||
@@ -8610,8 +9188,14 @@ namespace Oxide.Plugins
|
|||||||
// it lives in memory only — and its record is all there is left to remove.
|
// it lives in memory only — and its record is all there is left to remove.
|
||||||
wasThere = zm != null && zm.Call("CheckZoneID", e.Id) != null;
|
wasThere = zm != null && zm.Call("CheckZoneID", e.Id) != null;
|
||||||
|
|
||||||
|
// The dome first: ZoneDomes keeps it in its own data file, and a dome
|
||||||
|
// left there outlives the zone it was drawn over (D194).
|
||||||
|
RemoveDome(e);
|
||||||
|
|
||||||
if (wasThere)
|
if (wasThere)
|
||||||
zm.Call("EraseTemporaryZone", this, e.Id);
|
zm.Call("EraseTemporaryZone", this, e.Id);
|
||||||
|
|
||||||
|
_zoneQuietUntil.Remove(e.Id);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -8891,10 +9475,13 @@ namespace Oxide.Plugins
|
|||||||
|
|
||||||
foreach (WorldEntry e in due)
|
foreach (WorldEntry e in due)
|
||||||
{
|
{
|
||||||
|
RemoveDome(e);
|
||||||
|
|
||||||
if (zm != null && zm.Call("CheckZoneID", e.Id) != null)
|
if (zm != null && zm.Call("CheckZoneID", e.Id) != null)
|
||||||
zm.Call("EraseTemporaryZone", this, e.Id);
|
zm.Call("EraseTemporaryZone", this, e.Id);
|
||||||
|
|
||||||
_world.Remove(e.Id);
|
_world.Remove(e.Id);
|
||||||
|
_zoneQuietUntil.Remove(e.Id);
|
||||||
|
|
||||||
Dictionary<string, object> frame = Frame("world.expired", "event");
|
Dictionary<string, object> frame = Frame("world.expired", "event");
|
||||||
frame["id"] = e.Id;
|
frame["id"] = e.Id;
|
||||||
@@ -10430,10 +11017,34 @@ namespace Oxide.Plugins
|
|||||||
return new Dictionary<string, object>
|
return new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "betterChat", IntegrationBody(BetterChatPlugin()) },
|
{ "betterChat", IntegrationBody(BetterChatPlugin()) },
|
||||||
{ "popupNotifications", IntegrationBody(PopupPlugin()) }
|
{ "popupNotifications", IntegrationBody(PopupPlugin()) },
|
||||||
|
{ "zoneDomes", ZoneDomesBody() }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ZoneDomes and the domes helper (D194), live on every status frame like the other
|
||||||
|
/// optional mods: the site offers a dome only where both are here and patched.
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, object> ZoneDomesBody()
|
||||||
|
{
|
||||||
|
Dictionary<string, object> body = IntegrationBody(ZoneDomesPlugin());
|
||||||
|
var helper = new Dictionary<string, object> { { "state", ZoneDomesPlugin() == null && DomesHelperPlugin() != null ? "no-zonedomes" : DomesHelperState() } };
|
||||||
|
Plugin domes = DomesHelperPlugin();
|
||||||
|
|
||||||
|
if (domes != null)
|
||||||
|
{
|
||||||
|
helper["version"] = domes.Version.ToString();
|
||||||
|
|
||||||
|
if (domes.Call("StateReason") is string reason)
|
||||||
|
helper["reason"] = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
body["helper"] = helper;
|
||||||
|
body["ready"] = DomesReady();
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>A version only for a plugin that is loaded. Absent, not empty.</summary>
|
/// <summary>A version only for a plugin that is loaded. Absent, not empty.</summary>
|
||||||
private static Dictionary<string, object> IntegrationBody(Plugin plugin)
|
private static Dictionary<string, object> IntegrationBody(Plugin plugin)
|
||||||
{
|
{
|
||||||
|
|||||||
261
overlay/oxide/plugins/RunicGatewayDomes.cs
Normal file
261
overlay/oxide/plugins/RunicGatewayDomes.cs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
// Runic Gateway Domes — a helper for ZoneDomes, shipped beside the Runic Gateway bridge.
|
||||||
|
// Copyright (C) Runic Gateway contributors. GPL-3.0-or-later.
|
||||||
|
//
|
||||||
|
// ONE JOB (docs/modules/rust/PLAN_REDESIGNS.md §3.2, D168, D194):
|
||||||
|
//
|
||||||
|
// ZoneDomes draws a dome over a ZoneManager zone, and the bridge asks it to for an event zone
|
||||||
|
// whose step wants one. Its API was written for a person at a keyboard: `AddNewDome` and
|
||||||
|
// `RemoveExistingDome` both end by telling the player who asked how it went, with no check that
|
||||||
|
// there was one. Called by a plugin with no player, the dome is made and saved, and THEN the call
|
||||||
|
// throws — so the caller cannot tell a made dome from a failed one (plan §0.5, found on the rig).
|
||||||
|
//
|
||||||
|
// So this helper patches ZoneDomes with Harmony, the same way the zone helper patches ZoneManager
|
||||||
|
// (D181):
|
||||||
|
//
|
||||||
|
// * **A message with no player is not sent.** A prefix on ZoneDomes' `TranslateMessage(BasePlayer,
|
||||||
|
// string, string)` skips it when the player is null. Nothing else about the call changes, so
|
||||||
|
// `AddNewDome` returns its true or false normally.
|
||||||
|
// * **It says when ZoneDomes has finished starting.** ZoneDomes' own start (`InitializeDomes`)
|
||||||
|
// drops every dome whose zone does not exist yet and draws every one that does. A dome added
|
||||||
|
// BEFORE that is drawn a second time, and the first set of spheres stays in the world with
|
||||||
|
// nothing holding it. A postfix there raises `OnRgDomesReady`, and the bridge puts its domes
|
||||||
|
// back only after it.
|
||||||
|
//
|
||||||
|
// It is optional. Without it the bridge offers no dome and says why; the zones themselves are
|
||||||
|
// unaffected. `rgd.status` in the server console prints its state.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Oxide.Core;
|
||||||
|
using Oxide.Core.Plugins;
|
||||||
|
|
||||||
|
namespace Oxide.Plugins
|
||||||
|
{
|
||||||
|
[Info("Runic Gateway Domes", "Runic Gateway", "0.1.0")]
|
||||||
|
[Description("Helper for Runic Gateway: ZoneDomes can be called without a player, and says when it has started.")]
|
||||||
|
public class RunicGatewayDomes : RustPlugin
|
||||||
|
{
|
||||||
|
private const string HarmonyId = "runicgateway.domes";
|
||||||
|
|
||||||
|
/// <summary>The instance the static patches report to. Null while unloaded.</summary>
|
||||||
|
private static RunicGatewayDomes _instance;
|
||||||
|
|
||||||
|
private Harmony _harmony;
|
||||||
|
private MethodInfo _message;
|
||||||
|
private MethodInfo _initialize;
|
||||||
|
|
||||||
|
/// <summary><c>patched</c>, <c>unsupported</c> (ZoneDomes is here but not the shape patched), or <c>no-zonedomes</c>.</summary>
|
||||||
|
private string _state = "no-zonedomes";
|
||||||
|
private string _reason;
|
||||||
|
|
||||||
|
/// <summary>Whether ZoneDomes has finished its own start since it was patched. Main thread.</summary>
|
||||||
|
private bool _ready;
|
||||||
|
|
||||||
|
/// <summary>Set once the server is up, at this plugin's own <c>OnServerInitialized</c>.</summary>
|
||||||
|
private bool _serverUp;
|
||||||
|
|
||||||
|
// Patched as EARLY as possible — at load, or the moment ZoneDomes loads — so that at a real
|
||||||
|
// server start the patch is in before ZoneDomes' `OnServerInitialized` runs its start.
|
||||||
|
|
||||||
|
private void Loaded()
|
||||||
|
{
|
||||||
|
_instance = this;
|
||||||
|
Patch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServerInitialized(bool initial)
|
||||||
|
{
|
||||||
|
_serverUp = true;
|
||||||
|
|
||||||
|
// Loaded into a running server: ZoneDomes, if it is here, started long ago.
|
||||||
|
if (!initial)
|
||||||
|
MarkReadyNextTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unload()
|
||||||
|
{
|
||||||
|
Unpatch();
|
||||||
|
_instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPluginLoaded(Plugin plugin)
|
||||||
|
{
|
||||||
|
if (plugin == null || plugin.Name != "ZoneDomes")
|
||||||
|
return;
|
||||||
|
|
||||||
|
Unpatch();
|
||||||
|
Patch();
|
||||||
|
|
||||||
|
// A ZoneDomes loaded into a running server has run its start by now: the framework
|
||||||
|
// calls a hot-loaded plugin's `OnServerInitialized` before it tells anybody else it
|
||||||
|
// loaded. The tick is margin, and costs nothing.
|
||||||
|
if (_serverUp)
|
||||||
|
MarkReadyNextTick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPluginUnloaded(Plugin plugin)
|
||||||
|
{
|
||||||
|
if (plugin != null && plugin.Name == "ZoneDomes")
|
||||||
|
{
|
||||||
|
Unpatch();
|
||||||
|
_state = "no-zonedomes";
|
||||||
|
_reason = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── The API the bridge calls ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary><c>patched</c>, <c>unsupported</c> or <c>no-zonedomes</c>.</summary>
|
||||||
|
private string State() => _state;
|
||||||
|
|
||||||
|
/// <summary>Why the state is not <c>patched</c>, or null.</summary>
|
||||||
|
private string StateReason() => _reason;
|
||||||
|
|
||||||
|
/// <summary>Whether ZoneDomes is patched and past its own start, so a dome may be added.</summary>
|
||||||
|
private bool Ready() => _state == "patched" && _ready;
|
||||||
|
|
||||||
|
/// <summary>The helper's own version, for hello.</summary>
|
||||||
|
private string HelperVersion() => Version.ToString();
|
||||||
|
|
||||||
|
// ── Patching ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void Patch()
|
||||||
|
{
|
||||||
|
_ready = false;
|
||||||
|
Plugin zoneDomes = plugins.Find("ZoneDomes");
|
||||||
|
|
||||||
|
if (zoneDomes == null)
|
||||||
|
{
|
||||||
|
_state = "no-zonedomes";
|
||||||
|
_reason = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BindingFlags any = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
Type type = zoneDomes.GetType();
|
||||||
|
|
||||||
|
_message = type.GetMethod("TranslateMessage", any, null, new[] { typeof(BasePlayer), typeof(string), typeof(string) }, null);
|
||||||
|
_initialize = type.GetMethod("InitializeDomes", any, null, Type.EmptyTypes, null);
|
||||||
|
|
||||||
|
string missing =
|
||||||
|
_message == null ? "TranslateMessage(BasePlayer, string, string)" :
|
||||||
|
_initialize == null ? "InitializeDomes()" :
|
||||||
|
type.GetMethod("AddNewDome", any) == null ? "AddNewDome" :
|
||||||
|
type.GetMethod("RemoveExistingDome", any) == null ? "RemoveExistingDome" :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (missing != null)
|
||||||
|
{
|
||||||
|
Fail($"this ZoneDomes ({zoneDomes.Version}) has no {missing}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_harmony = new Harmony(HarmonyId);
|
||||||
|
_harmony.Patch(_message, prefix: new HarmonyMethod(
|
||||||
|
typeof(RunicGatewayDomes).GetMethod(nameof(BeforeMessage), BindingFlags.Static | BindingFlags.NonPublic)));
|
||||||
|
_harmony.Patch(_initialize, postfix: new HarmonyMethod(
|
||||||
|
typeof(RunicGatewayDomes).GetMethod(nameof(AfterInitialize), BindingFlags.Static | BindingFlags.NonPublic)));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Fail($"Harmony could not patch ZoneDomes: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_state = "patched";
|
||||||
|
_reason = null;
|
||||||
|
Puts($"patched ZoneDomes {zoneDomes.Version}: its API can be called without a player");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Unpatch()
|
||||||
|
{
|
||||||
|
if (_harmony != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_message != null)
|
||||||
|
_harmony.Unpatch(_message, HarmonyPatchType.Prefix, HarmonyId);
|
||||||
|
|
||||||
|
if (_initialize != null)
|
||||||
|
_harmony.Unpatch(_initialize, HarmonyPatchType.Postfix, HarmonyId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PrintWarning($"could not remove the ZoneDomes patches: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_harmony = null;
|
||||||
|
_message = null;
|
||||||
|
_initialize = null;
|
||||||
|
_ready = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fail(string reason)
|
||||||
|
{
|
||||||
|
Unpatch();
|
||||||
|
_state = "unsupported";
|
||||||
|
_reason = reason;
|
||||||
|
PrintWarning($"{reason}. Runic Gateway will not offer a dome over its zones on this server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkReadyNextTick()
|
||||||
|
{
|
||||||
|
NextTick(() =>
|
||||||
|
{
|
||||||
|
if (_state != "patched" || _ready)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ready = true;
|
||||||
|
Interface.CallHook("OnRgDomesReady");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── The patches ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Harmony prefix on <c>ZoneDomes.TranslateMessage(BasePlayer, string, string)</c>.
|
||||||
|
/// Returning false skips the message; a real player still gets it.
|
||||||
|
/// </summary>
|
||||||
|
private static bool BeforeMessage(BasePlayer player)
|
||||||
|
{
|
||||||
|
return player != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Harmony postfix on <c>ZoneDomes.InitializeDomes</c>. Anything thrown here would surface
|
||||||
|
/// inside ZoneDomes, so nothing is allowed to.
|
||||||
|
/// </summary>
|
||||||
|
private static void AfterInitialize()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunicGatewayDomes self = _instance;
|
||||||
|
|
||||||
|
if (self == null || self._state != "patched")
|
||||||
|
return;
|
||||||
|
|
||||||
|
self._ready = true;
|
||||||
|
Interface.CallHook("OnRgDomesReady");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Never ZoneDomes' problem.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Console ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ConsoleCommand("rgd.status")]
|
||||||
|
private void StatusCommand(ConsoleSystem.Arg arg)
|
||||||
|
{
|
||||||
|
if (arg.Connection != null && !arg.IsAdmin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
arg.ReplyWith($"Runic Gateway Domes {Version}: {_state}{(_state == "patched" ? (_ready ? ", ready" : ", waiting for ZoneDomes to start") : "")}{(_reason != null ? $" ({_reason})" : "")}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user