diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index 32b679c..4217de3 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -1875,7 +1875,22 @@ namespace Oxide.Plugins // two meanings of one word would be two answers to "how many NPCs did I kill". // `animalKills` counts it AGAIN, separately (§5.2): the public NPC column keeps its // meaning mid-wipe, and the site takes the human NPCs as the difference. - if (entity is BaseNpc) + // + // Rust's own animals are now `Rust.Ai.Gen2.BaseNPC2` — stag, boar, wolf, bears, big cats, + // crocodile, farm animals, critters, sea life — and not `BaseNpc`, which is all this + // tested until the RunicNPC 9d walk found no animal kill had counted at all. `BaseNpc` + // stays for whatever still derives from it. `ScientistNPC2` is the one person among + // them, so it is a human NPC kill, not an animal. + if (entity is Rust.Ai.Gen2.ScientistNPC2) + { + Tally tally = TallyFor(attacker); + tally.NpcKills++; + tally.Killed(info, entity, false); + CreditKill(attacker, false); + return; + } + + if (entity is BaseNpc || entity is Rust.Ai.Gen2.BaseNPC2) { Tally tally = TallyFor(attacker); tally.NpcKills++; @@ -2065,55 +2080,56 @@ namespace Oxide.Plugins }); } - // A syringe on a downed player raises `OnPlayerRevive` and then `OnHealingItemUse` for the - // same use; one act of help is one heal. The revive is remembered for the frame it happened in. - private ulong _reviveHealer; - private ulong _reviveTarget; - private int _reviveFrame = -1; + // A syringe on a downed player raises `OnHealingItemUse` and then, from inside the same + // `GiveEffectsTo`, `OnPlayerRevive`; one act of help is one heal. The last one counted is + // remembered for the frame it happened in, and the other hook skips the same pair. + private ulong _helpHealer; + private ulong _helpTarget; + private int _helpFrame = -1; - /// - /// Void: a non-null return refuses the heal. is what is healed; - /// the tool's owner is who heals. Healing yourself is not a heal (D174). - /// - /// - /// The target is taken as object: the game's `GiveEffectsTo` now takes an - /// `IMedicalToolTarget`, which is not always a player, and a parameter typed - /// `BasePlayer` would simply never be called for the hook's current shape. Only a - /// player healed counts. - /// - /// - private void OnHealingItemUse(MedicalTool tool, object target) + /// Counts one act of help unless it is yourself, an NPC, or the other hook already did this frame. + private void CountHelp(BasePlayer healer, BasePlayer player) { - MarkHook("OnHealingItemUse"); - - BasePlayer player = target as BasePlayer; - BasePlayer healer = tool != null ? tool.GetOwnerPlayer() : null; - if (healer == null || healer.IsNpc || player == null || player.IsNpc || (ulong)healer.userID == (ulong)player.userID) return; - if (_reviveFrame == UnityEngine.Time.frameCount - && _reviveHealer == (ulong)healer.userID && _reviveTarget == (ulong)player.userID) + int frame = UnityEngine.Time.frameCount; + + if (_helpFrame == frame && _helpHealer == (ulong)healer.userID && _helpTarget == (ulong)player.userID) return; + _helpHealer = healer.userID; + _helpTarget = player.userID; + _helpFrame = frame; + TallyFor(healer).Healed++; } + /// + /// Void: a non-null return refuses the heal. Healing yourself is not a heal (D174). + /// + /// + /// Rust's `MedicalTool.GiveEffectsTo(fromPlayer, toTarget)` raises it as + /// (tool, fromPlayer, toTarget). Until the RunicNPC 9d walk this took two + /// parameters, so the second — the healer — arrived as the target, every heal looked like + /// healing yourself, and none counted. The target is object: `IMedicalToolTarget` + /// is not always a player, and only a player healed counts. + /// + /// + private void OnHealingItemUse(MedicalTool tool, BasePlayer fromPlayer, object target) + { + MarkHook("OnHealingItemUse"); + + CountHelp(fromPlayer ?? (tool != null ? tool.GetOwnerPlayer() : null), target as BasePlayer); + } + /// Void: a non-null return refuses the revive. Reviving yourself is not possible, and is not counted. private void OnPlayerRevive(BasePlayer reviver, BasePlayer player) { MarkHook("OnPlayerRevive"); - if (reviver == null || reviver.IsNpc || player == null || player.IsNpc - || (ulong)reviver.userID == (ulong)player.userID) - return; - - _reviveHealer = reviver.userID; - _reviveTarget = player.userID; - _reviveFrame = UnityEngine.Time.frameCount; - - TallyFor(reviver).Healed++; + CountHelp(reviver, player); } /// No return behaviour. One rocket leaving a launcher. diff --git a/overlay/oxide/plugins/RunicGatewayZones.cs b/overlay/oxide/plugins/RunicGatewayZones.cs index 33c2ecb..ed28130 100644 --- a/overlay/oxide/plugins/RunicGatewayZones.cs +++ b/overlay/oxide/plugins/RunicGatewayZones.cs @@ -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; + /// ZoneManager's private UpdateHookSubscriptions, or null when this ZoneManager has none (the second job is then off). + private MethodInfo _updateHooks; + /// /// What the bridge reads through : patched, unsupported /// (ZoneManager is here but not the shape this helper patches), or no-zonemanager. @@ -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>(_pending); _pending.Clear(); + // The second job first: the hooks that enforce the new zones' flags, then the players. + RefreshHooks(); + foreach (KeyValuePair entry in batch) Sweep(entry.Key, entry.Value); } + /// + /// 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. + /// + 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)")}"); } } }