// 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})" : "")}");
}
}
}