Merge pull request 'feat(bridge): a zone's ZoneManager options, its messages, and a ZoneDomes dome (PLAN_REDESIGNS §3)' (#21) from feat/zones-domes into edge
Reviewed-on: #21
This commit is contained in:
@@ -29,8 +29,8 @@
|
||||
# exact commit being released.
|
||||
#
|
||||
# 2. A FRAMEWORK-NEUTRAL LAYOUT. The tarball holds one directory,
|
||||
# `runicgateway-rust-plugin/`, with `RunicGateway.cs`, `RunicGatewayZones.cs`
|
||||
# and `manifest.json` at its top. Every `.cs` in it is listed in the
|
||||
# `runicgateway-rust-plugin/`, with `RunicGateway.cs`, its helpers
|
||||
# (`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
|
||||
# `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
|
||||
@@ -40,7 +40,8 @@
|
||||
# `oxide.plugins` / `c.plugins` on a server names the release it runs. The
|
||||
# committed file keeps its placeholder; the manifest records the commit.
|
||||
# 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
|
||||
# hello to know which job it does.
|
||||
#
|
||||
@@ -80,7 +81,7 @@ env:
|
||||
PLUGIN: overlay/oxide/plugins/RunicGateway.cs
|
||||
# The helpers shipped beside the bridge (D168, D182). Optional at runtime —
|
||||
# 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"
|
||||
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/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
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
```json
|
||||
|
||||
@@ -244,7 +244,8 @@ namespace Oxide.Plugins
|
||||
"OnClanCreated", "OnClanDisbanded", "OnClanMemberAdded",
|
||||
"OnClanMemberLeft", "OnClanMemberKicked", "OnClanColorChanged",
|
||||
"OnKitRedeemed",
|
||||
"OnEntitySpawned", "OnEntityKill"
|
||||
"OnEntitySpawned", "OnEntityKill",
|
||||
"OnEnterZone", "OnExitZone", "OnRgDomesReady"
|
||||
};
|
||||
|
||||
private void Init()
|
||||
@@ -1173,6 +1174,17 @@ namespace Oxide.Plugins
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -4527,6 +4539,22 @@ namespace Oxide.Plugins
|
||||
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>
|
||||
private void NotePluginChange(Plugin plugin, bool loaded)
|
||||
{
|
||||
@@ -4548,6 +4576,11 @@ namespace Oxide.Plugins
|
||||
if (plugin != null && plugin != this && plugin.Name == "ZoneManager")
|
||||
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
|
||||
// forgotten our getter. Registering again is the only way back (§33.1).
|
||||
if (plugin != null && plugin != this && plugin.Name == "BetterChat")
|
||||
@@ -4570,6 +4603,9 @@ namespace Oxide.Plugins
|
||||
if (plugin != null && plugin.Name == "BetterChat")
|
||||
_titlesRegistered = false;
|
||||
|
||||
if (IsZonePlugin(plugin))
|
||||
NextTick(ResendHello);
|
||||
|
||||
ConfigWriteJob job = _configJob;
|
||||
|
||||
if (job == null || plugin == null)
|
||||
@@ -7446,6 +7482,24 @@ namespace Oxide.Plugins
|
||||
public float Radius;
|
||||
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>
|
||||
public long DeadlineMs;
|
||||
|
||||
@@ -7542,6 +7596,401 @@ namespace Oxide.Plugins
|
||||
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>
|
||||
/// 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
|
||||
@@ -7904,6 +8353,86 @@ namespace Oxide.Plugins
|
||||
if (name.Length > 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
|
||||
{
|
||||
Id = $"rg-{ShortRun(runId)}-{NowMs() % 100000000L}-{++_zoneSerial}",
|
||||
@@ -7915,6 +8444,15 @@ namespace Oxide.Plugins
|
||||
Z = point.z,
|
||||
Radius = (float)radius,
|
||||
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,
|
||||
BootId = _bootId,
|
||||
WipeId = _wipeId,
|
||||
@@ -7927,11 +8465,20 @@ namespace Oxide.Plugins
|
||||
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;
|
||||
SaveWorld();
|
||||
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}");
|
||||
EmitWorldPlaced(reqId, new List<WorldEntry> { entry }, false);
|
||||
}
|
||||
@@ -7959,13 +8506,35 @@ namespace Oxide.Plugins
|
||||
/// </summary>
|
||||
private bool CreateZone(Plugin zm, WorldEntry entry)
|
||||
{
|
||||
string[] args =
|
||||
var args = new List<string>
|
||||
{
|
||||
"name", entry.Name ?? "Event zone",
|
||||
"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;
|
||||
}
|
||||
@@ -7995,9 +8564,14 @@ namespace Oxide.Plugins
|
||||
continue;
|
||||
|
||||
if (CreateZone(zm, e))
|
||||
{
|
||||
restored++;
|
||||
_zoneQuietUntil[e.Id] = NowMs() + ZoneQuietMs;
|
||||
}
|
||||
else
|
||||
{
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (restored > 0 || failed > 0)
|
||||
@@ -8005,6 +8579,10 @@ namespace Oxide.Plugins
|
||||
_lastWorldSummary = $"zones after {why}: {restored} re-created, {failed} failed";
|
||||
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>
|
||||
@@ -8227,8 +8805,14 @@ namespace Oxide.Plugins
|
||||
// it lives in memory only — and its record is all there is left to remove.
|
||||
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)
|
||||
zm.Call("EraseTemporaryZone", this, e.Id);
|
||||
|
||||
_zoneQuietUntil.Remove(e.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -8508,10 +9092,13 @@ namespace Oxide.Plugins
|
||||
|
||||
foreach (WorldEntry e in due)
|
||||
{
|
||||
RemoveDome(e);
|
||||
|
||||
if (zm != null && zm.Call("CheckZoneID", e.Id) != null)
|
||||
zm.Call("EraseTemporaryZone", this, e.Id);
|
||||
|
||||
_world.Remove(e.Id);
|
||||
_zoneQuietUntil.Remove(e.Id);
|
||||
|
||||
Dictionary<string, object> frame = Frame("world.expired", "event");
|
||||
frame["id"] = e.Id;
|
||||
@@ -10047,10 +10634,34 @@ namespace Oxide.Plugins
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "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>
|
||||
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