fix(zones): an event's zone enforces its flags (RunicGatewayZones 0.2.0, runicnpc 9d)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 13s

ZoneManager subscribes the hooks that enforce its flags only for the zones it
had when it loaded; a zone made later through its API, which is every zone an
event opens, never re-subscribes them, so an event's PvpGod or NoBuild zone
enforced nothing. The helper's postfix, which already sees every zone created,
updated or loaded, now has ZoneManager run its own UpdateHookSubscriptions on
the next tick, before entering the players already inside. A ZoneManager
without that method is warned about and reported by rgz.status; the first job
still works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-07 13:41:57 -05:00
parent e5f04e0c0c
commit a785ad91ef

View File

@@ -1,7 +1,7 @@
// Runic Gateway Zones — a helper for ZoneManager, shipped beside the Runic Gateway bridge.
// Copyright (C) Runic Gateway contributors. GPL-3.0-or-later.
//
// ONE JOB (docs/modules/rust/PLAN_FIXES.md F12, D168, D181, D182):
// ITS FIRST JOB (docs/modules/rust/PLAN_FIXES.md F12, D168, D181, D182):
//
// ZoneManager counts a player as inside a zone only when the zone's trigger fires on ENTRY. A
// zone created, updated or loaded around somebody who is already standing there never fires it,
@@ -27,6 +27,17 @@
// * **It fixes every zone on the server**, not only the bridge's. That is deliberate: the fault is
// ZoneManager's, and it is the same fault for an arena another plugin made.
//
// A SECOND JOB (RunicNPC stage 9d walk, the org lead's call, 2026-10-07):
//
// ZoneManager subscribes the hooks that enforce its flags (`OnEntityTakeDamage` for PvpGod and the
// other damage flags, `OnEntityBuilt` for NoBuild, and so on) only for the flags some zone had when
// ZoneManager loaded, or when a flag is toggled by command. A zone made later through its API —
// which is every zone an event opens — never re-subscribes them, so on a server with no such zone
// already, an event's PvpGod or NoBuild zone enforces nothing: the 9d walk shot a player dead and
// built a foundation inside one. The same postfix that sees every zone made therefore also has
// ZoneManager run its own `UpdateHookSubscriptions` on the next tick. ZoneManager decides what to
// subscribe; this only asks it to look.
//
// It is optional. The bridge never depends on it to load: without it — deleted, or loaded but
// unable to find what it patches after a ZoneManager update — the bridge scores the zones it made
// by `IsPositionInZone` instead (D182), and reports this helper's state at hello so the site can
@@ -41,8 +52,8 @@ using UnityEngine;
namespace Oxide.Plugins
{
[Info("Runic Gateway Zones", "Runic Gateway", "0.1.0")]
[Description("Helper for Runic Gateway: ZoneManager counts players already inside a zone when it is created, updated or loaded.")]
[Info("Runic Gateway Zones", "Runic Gateway", "0.2.0")]
[Description("Helper for Runic Gateway: ZoneManager counts players already inside a zone when it is created, updated or loaded, and enforces a new zone's flags.")]
public class RunicGatewayZones : RustPlugin
{
private const string HarmonyId = "runicgateway.zones";
@@ -61,6 +72,9 @@ namespace Oxide.Plugins
private FieldInfo _definition;
private PropertyInfo _id;
/// <summary>ZoneManager's private <c>UpdateHookSubscriptions</c>, or null when this ZoneManager has none (the second job is then off).</summary>
private MethodInfo _updateHooks;
/// <summary>
/// What the bridge reads through <see cref="State"/>: <c>patched</c>, <c>unsupported</c>
/// (ZoneManager is here but not the shape this helper patches), or <c>no-zonemanager</c>.
@@ -134,6 +148,7 @@ namespace Oxide.Plugins
MethodInfo initialize = zoneType?.GetMethod("InitializeZone", any);
_getZone = zmType.GetMethod("GetZoneByID", any, null, new[] { typeof(string) }, null);
_updateHooks = zmType.GetMethod("UpdateHookSubscriptions", any, null, Type.EmptyTypes, null);
_definition = zoneType?.GetField("definition", any);
_id = _definition?.FieldType.GetProperty("Id", any);
_enter = zoneType == null
@@ -169,10 +184,14 @@ namespace Oxide.Plugins
_state = "patched";
_reason = null;
Puts($"patched ZoneManager {_zoneManager.Version}: a zone takes in the players already inside it");
Puts($"patched ZoneManager {_zoneManager.Version}: a zone takes in the players already inside it" +
(_updateHooks != null ? ", and a new zone's flags are enforced" : ""));
if (_updateHooks == null)
PrintWarning($"this ZoneManager ({_zoneManager.Version}) has no UpdateHookSubscriptions: a zone made after it loaded may not enforce its flags until ZoneManager reloads");
// The zones ZoneManager loaded before this helper did were initialised unwatched —
// which, at a server start, is all of them. Sweep them once now.
RefreshHooks();
SweepAll();
}
@@ -258,10 +277,32 @@ namespace Oxide.Plugins
var batch = new List<KeyValuePair<string, object>>(_pending);
_pending.Clear();
// The second job first: the hooks that enforce the new zones' flags, then the players.
RefreshHooks();
foreach (KeyValuePair<string, object> entry in batch)
Sweep(entry.Key, entry.Value);
}
/// <summary>
/// Main thread. Has ZoneManager subscribe the hooks its zones' flags now need. ZoneManager
/// only adds subscriptions here, and subscribing a hook twice is a no-op.
/// </summary>
private void RefreshHooks()
{
if (_updateHooks == null || _zoneManager == null)
return;
try
{
_updateHooks.Invoke(_zoneManager, null);
}
catch (Exception ex)
{
PrintWarning($"could not have ZoneManager refresh its hooks: {(ex.InnerException ?? ex).Message}");
}
}
private void SweepAll()
{
var ids = _zoneManager.Call("GetZoneIDs") as string[];
@@ -328,7 +369,8 @@ namespace Oxide.Plugins
if (arg.Connection != null && !arg.IsAdmin)
return;
arg.ReplyWith($"Runic Gateway Zones {Version}: {_state}{(_reason != null ? $" ({_reason})" : "")}");
arg.ReplyWith($"Runic Gateway Zones {Version}: {_state}{(_reason != null ? $" ({_reason})" : "")}" +
$"; new zones' flags {(_updateHooks != null ? "enforced" : "NOT refreshed (no UpdateHookSubscriptions)")}");
}
}
}