// 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"; /// The instance the static patches report to. Null while unloaded. private static RunicGatewayDomes _instance; private Harmony _harmony; private MethodInfo _message; private MethodInfo _initialize; /// patched, unsupported (ZoneDomes is here but not the shape patched), or no-zonedomes. private string _state = "no-zonedomes"; private string _reason; /// Whether ZoneDomes has finished its own start since it was patched. Main thread. private bool _ready; /// Set once the server is up, at this plugin's own OnServerInitialized. 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 ───────────────────────────────────────────── /// patched, unsupported or no-zonedomes. private string State() => _state; /// Why the state is not patched, or null. private string StateReason() => _reason; /// Whether ZoneDomes is patched and past its own start, so a dome may be added. private bool Ready() => _state == "patched" && _ready; /// The helper's own version, for hello. 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 ────────────────────────────────────────────────────────── /// /// Harmony prefix on ZoneDomes.TranslateMessage(BasePlayer, string, string). /// Returning false skips the message; a real player still gets it. /// private static bool BeforeMessage(BasePlayer player) { return player != null; } /// /// Harmony postfix on ZoneDomes.InitializeDomes. Anything thrown here would surface /// inside ZoneDomes, so nothing is allowed to. /// 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})" : "")}"); } } }