world.zone gains flags, settings, enterMessage, leaveMessage, delivery,
format and dome (PROTOCOL §19.11). Flags are checked against the live
ZoneManager's own list, read through its API call ZoneFieldListRaw() (D211),
and refused bad-option rather than dropped; settings are an allowlist of
five with bounds. Both are kept on the registry entry and re-applied when
a zone is re-created.
The bridge says a zone's enter and leave messages itself (D193), as chat
or a popup (chat when PopupNotifications is missing), and stays quiet for
5 s after re-creating a zone.
RunicGatewayDomes.cs, a new helper, patches ZoneDomes so its API can be
called without a player, and raises OnRgDomesReady after ZoneDomes' own
start; the bridge puts its domes back only after it, so none is drawn
twice. Domes are removed before their zone at expiry and revert.
hello carries zoneManager { version, flags }, integrations.zoneDomes the
ZoneDomes and helper state, and hello is sent again when ZoneManager,
ZoneDomes or either helper loads or unloads. The release ships the new
helper beside the zone helper.
Not compiled or walked on a rig: the panel's file API was down on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
262 lines
10 KiB
C#
262 lines
10 KiB
C#
// 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})" : "")}");
|
|
}
|
|
}
|
|
}
|