feat(bridge): Guarded Crate on protocol 14 (PLAN_REDESIGNS §14.3.5)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 9s
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 9s
Stage 6 of the §14.3 integration plan. Spec: docs rust-link/PROTOCOL.md §20.9.
- gc.started, gc.ended (the clearer, or nobody) and gc.failed from Guarded
Crate's three hooks; the start's key joins exactly, inside the call.
- gc.place { key, event, location as a world call } runs `<command> position
<event> x,y,z`; refuses npcspawn-missing itself, since Guarded Crate's
`position` does not check and the crate would land unguarded (D364).
- The running set is checked against Guarded Crate's private list (by
reflection): the hotload seed, `stop all` (no hook), and which event ended.
- Hello's integrations.guardedCrate with npcSpawn and the events from its data
file; the tally's guardedWon (Warden); map.live's guarded rows; rg.world.
- Guarded Crate's cargo plane no longer says airdrop.called.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -126,6 +126,14 @@ after DT loads it is still finding its spots, and a start then is answered "not
|
||||
that cannot run another plugin's console command from code (`rg.world` says `consolePlugins=False`)
|
||||
cannot open one.
|
||||
|
||||
Bazz3l's `Guarded Crate` is optional too, and needs `NpcSpawn` (a free Codefling download, not shipped
|
||||
here) for its guards. Where both are loaded, a guarded crate landing, being cleared and timing out
|
||||
reach the site's feed, its map and the *Warden* title, and an event can place one (`gc.place`) at a
|
||||
monument or a point through Guarded Crate's own `gcrate position` console command. A placed crate is
|
||||
kept when the event ends: Guarded Crate has no way to end one event alone. Without NpcSpawn a placement
|
||||
is refused, because the crate would land unguarded; and, as for Dangerous Treasures, a framework with
|
||||
`consolePlugins=False` cannot place one.
|
||||
|
||||
Rust's own world events need no plugin at all. The patrol helicopter, Bradley, the cargo ship, the
|
||||
Chinook, airdrops, locked crates, the Travelling Vendor, the satellite and the Deep Sea reach the
|
||||
site's feed and its event triggers on every server, and an event can call one of them (`world.call`).
|
||||
|
||||
@@ -74,7 +74,10 @@ namespace Oxide.Plugins
|
||||
/// <c>integrations.dynamicPvp</c>; and Dangerous Treasures (§20.8, PLAN_REDESIGNS.md
|
||||
/// §14.3.4): <c>dt.started</c>, <c>dt.won</c> and <c>dt.ended</c>, <c>dt.start</c>,
|
||||
/// <c>map.live</c>'s <c>treasure</c> rows, the tally's <c>treasuresWon</c> and
|
||||
/// <c>integrations.dangerousTreasures</c>.
|
||||
/// <c>integrations.dangerousTreasures</c>; and Guarded Crate (§20.9, PLAN_REDESIGNS.md
|
||||
/// §14.3.5): <c>gc.started</c>, <c>gc.ended</c> and <c>gc.failed</c>, <c>gc.place</c>,
|
||||
/// <c>map.live</c>'s <c>guarded</c> rows, the tally's <c>guardedWon</c> and
|
||||
/// <c>integrations.guardedCrate</c> with NpcSpawn beside it.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 14;
|
||||
@@ -282,7 +285,9 @@ namespace Oxide.Plugins
|
||||
// Protocol 14, Dynamic PVP's zones (§20.7).
|
||||
"OnCreatedDynamicPVP", "OnDeletedDynamicPVP",
|
||||
// Protocol 14, Dangerous Treasures (§20.8).
|
||||
"OnDangerousEventStarted", "OnDangerousEventWon", "OnDangerousEventEnded"
|
||||
"OnDangerousEventStarted", "OnDangerousEventWon", "OnDangerousEventEnded",
|
||||
// Protocol 14, Guarded Crate (§20.9).
|
||||
"OnGuardedCrateEventStart", "OnGuardedCrateEventEnded", "OnGuardedCrateEventFailed"
|
||||
};
|
||||
|
||||
private void Init()
|
||||
@@ -577,6 +582,7 @@ namespace Oxide.Plugins
|
||||
LoadRewards();
|
||||
LoadWorldCalls();
|
||||
LoadDtStarts();
|
||||
LoadGcPlacements();
|
||||
}
|
||||
|
||||
// ---- the outbound queue ----
|
||||
@@ -1081,6 +1087,12 @@ namespace Oxide.Plugins
|
||||
HandleDtStart(cmd, reqId);
|
||||
break;
|
||||
|
||||
case "gc.place":
|
||||
// Protocol 14 (PROTOCOL.md §20.9, D364, D373). One guarded crate, placed through
|
||||
// Guarded Crate's own console command; kept when the run ends.
|
||||
HandleGcPlace(cmd, reqId);
|
||||
break;
|
||||
|
||||
case "tally.open":
|
||||
// Protocol 10 (PLAN.md §29). Who takes part in a run, counted by the game.
|
||||
HandleTallyOpen(cmd, reqId);
|
||||
@@ -1649,6 +1661,7 @@ namespace Oxide.Plugins
|
||||
SeedPvpZones("load");
|
||||
SeedTreasures("load");
|
||||
NoteDtSeen(DangerousTreasuresPlugin() != null);
|
||||
SeedGuardedCrates("load");
|
||||
|
||||
// Protocol 14: the console probe (D363) and the world events' sweep. Before hello, which
|
||||
// carries what the probe found.
|
||||
@@ -2892,6 +2905,9 @@ namespace Oxide.Plugins
|
||||
|
||||
/// <summary>Protocol 14 (D375): Dangerous Treasures chests this player emptied (Treasure Hunter).</summary>
|
||||
public int TreasuresWon;
|
||||
|
||||
/// <summary>Protocol 14 (D375): guarded crates this player cleared, by killing the last guard (Warden).</summary>
|
||||
public int GuardedWon;
|
||||
public readonly Dictionary<string, int> Crafted = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>Stage 4 (D247): kills of RunicNPC's NPCs, by profile name. Also in <see cref="NpcKills"/>.</summary>
|
||||
@@ -2959,7 +2975,7 @@ namespace Oxide.Plugins
|
||||
{
|
||||
return Gathered.Count == 0 && NpcKills == 0 && Structures == 0
|
||||
&& AnimalKills == 0 && WeaponKills.Count == 0 && Headshots == 0
|
||||
&& BestPvpM <= 0f && BestPveM <= 0f && CratesHacked == 0 && DropsLooted == 0 && TreasuresWon == 0
|
||||
&& BestPvpM <= 0f && BestPveM <= 0f && CratesHacked == 0 && DropsLooted == 0 && TreasuresWon == 0 && GuardedWon == 0
|
||||
&& Crafted.Count == 0 && Built == 0 && Repaired == 0 && Healed == 0
|
||||
&& Rockets == 0 && Explosives == 0 && Missions == 0 && NpcProfileKills.Count == 0
|
||||
&& !HasBalance;
|
||||
@@ -3066,6 +3082,8 @@ namespace Oxide.Plugins
|
||||
frame["dropsLooted"] = tally.DropsLooted;
|
||||
if (tally.TreasuresWon > 0)
|
||||
frame["treasuresWon"] = tally.TreasuresWon;
|
||||
if (tally.GuardedWon > 0)
|
||||
frame["guardedWon"] = tally.GuardedWon;
|
||||
if (tally.Crafted.Count > 0)
|
||||
frame["crafted"] = tally.Crafted;
|
||||
if (tally.Built > 0)
|
||||
@@ -5196,7 +5214,8 @@ namespace Oxide.Plugins
|
||||
// Economics too: hello carries its starting balance, which the richest column needs;
|
||||
// and Backpacks, whose sizes are what a Bigger backpack step offers; and TruePVE, which a
|
||||
// zone step's PvP rules are refused without (§14.3.2); and Dynamic PVP (§14.3.3).
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin) || IsDangerousTreasures(plugin))
|
||||
// Guarded Crate and NpcSpawn too: a placement is refused without either (D364).
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin) || IsDangerousTreasures(plugin) || IsGuardedCrate(plugin))
|
||||
NextTick(ResendHello);
|
||||
|
||||
// Dynamic PVP raises its creates as it opens its zones, which at a load are its
|
||||
@@ -5239,9 +5258,13 @@ namespace Oxide.Plugins
|
||||
if (plugin != null && plugin.Name == "BetterChat")
|
||||
_titlesRegistered = false;
|
||||
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin) || IsDangerousTreasures(plugin))
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin) || IsDangerousTreasures(plugin) || IsGuardedCrate(plugin))
|
||||
NextTick(ResendHello);
|
||||
|
||||
// Guarded Crate's Unload ended every event WITHOUT a hook.
|
||||
if (plugin != null && plugin != this && plugin.Name == "GuardedCrate")
|
||||
_guardedCrates.Clear();
|
||||
|
||||
// Its Unload deleted every zone, each with its hook; whatever is left was missed.
|
||||
if (IsDynamicPvp(plugin))
|
||||
_pvpZones.Clear();
|
||||
@@ -9394,6 +9417,687 @@ namespace Oxide.Plugins
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
// ---- Guarded Crate, with NpcSpawn (PLAN_REDESIGNS §14.3.5, D361, D364, D373) ----
|
||||
//
|
||||
// Bazz3l's Guarded Crate drops a locked crate from a cargo plane and rings it with
|
||||
// NpcSpawn's scientists; whoever kills the last guard has cleared it. The bridge says when
|
||||
// one starts, is cleared and times out, for the feed, the map and the tally (Warden), and
|
||||
// places one for a run's step through Guarded Crate's own console command. Its source
|
||||
// (2.0.17) shapes it:
|
||||
//
|
||||
// * `OnGuardedCrateEventStart(Vector3)` is raised INSIDE the call that creates the
|
||||
// event, so a start our `gcrate position` caused is heard before `ConsoleSystem.Run`
|
||||
// returns: the key's join is exact, as Dangerous Treasures' is (§14.3.18).
|
||||
// * `OnGuardedCrateEventEnded(player, crate)` when the last guard dies, crediting that
|
||||
// guard's killer; the player is null when the guards went some other way (NpcSpawn
|
||||
// reloaded), which is "no winner". It does not say which event, so the event is
|
||||
// found by reading Guarded Crate's own list (below), else by the crate's position.
|
||||
// `OnGuardedCrateEventFailed(Vector3)` is the timeout.
|
||||
// * **Its unload and `gcrate stop all` end every event and raise no hook.** So the
|
||||
// running set is cleared when it unloads, and checked against its own list before
|
||||
// the map is drawn; a hotload seeds from that list too.
|
||||
// * **`gcrate position` does not check NpcSpawn** (only `start` does), so without it a
|
||||
// placed crate would land with no guards and be free to hack. The bridge refuses that
|
||||
// itself (D364).
|
||||
// * The events are rows of its DATA file, not its config, and its console command is a
|
||||
// config value. Both are read from their files, as Economics' config is.
|
||||
|
||||
private const string GcStartedKind = "gc.started";
|
||||
private const string GcEndedKind = "gc.ended";
|
||||
private const string GcFailedKind = "gc.failed";
|
||||
|
||||
/// <summary>Guarded Crate's console command when its config does not say.</summary>
|
||||
private const string GcDefaultCommand = "gcrate";
|
||||
|
||||
/// <summary>How far from a running event its crate or winner may be and still be placed at it.</summary>
|
||||
private const float GcEndReach = 100f;
|
||||
|
||||
/// <summary>How long a placement's key is remembered: seven days, as a payment's is.</summary>
|
||||
private const long GcPlaceMemoryMs = 7L * 24L * 60L * 60L * 1000L;
|
||||
|
||||
/// <summary>One running guarded crate: where, and the step's key when a step placed it.</summary>
|
||||
private sealed class GuardedCrateEvent
|
||||
{
|
||||
public UnityEngine.Vector3 At;
|
||||
public string Key;
|
||||
}
|
||||
|
||||
/// <summary>The running guarded crates, oldest first. Main thread only.</summary>
|
||||
private readonly List<GuardedCrateEvent> _guardedCrates = new List<GuardedCrateEvent>();
|
||||
|
||||
/// <summary>One placement a step asked for, kept for its key. Persisted as a DT start is.</summary>
|
||||
private sealed class GcPlacement
|
||||
{
|
||||
public string Key;
|
||||
public string Event;
|
||||
public float X;
|
||||
public float Z;
|
||||
public long AtMs;
|
||||
}
|
||||
|
||||
private Dictionary<string, GcPlacement> _gcPlacements = new Dictionary<string, GcPlacement>(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>The key of the `gcrate position` this plugin is running now, for the start hook. Null otherwise.</summary>
|
||||
private string _gcStarting;
|
||||
|
||||
/// <summary>The event the start hook heard while our `gcrate position` ran.</summary>
|
||||
private GuardedCrateEvent _gcOpened;
|
||||
|
||||
/// <summary>Guarded Crate's events (its data file) and console command (its config), each read again only when its file changes.</summary>
|
||||
private List<string> _gcEventNames;
|
||||
private DateTime _gcEventNamesAt;
|
||||
private string _gcCommand;
|
||||
private DateTime _gcCommandAt;
|
||||
|
||||
/// <summary>Whether Guarded Crate's own list of events could be read the last time it was asked.</summary>
|
||||
private bool _gcListReadable;
|
||||
private bool _gcListSaid;
|
||||
|
||||
private Plugin GuardedCratePlugin()
|
||||
{
|
||||
return plugins.Find("GuardedCrate");
|
||||
}
|
||||
|
||||
private Plugin NpcSpawnPlugin()
|
||||
{
|
||||
return plugins.Find("NpcSpawn");
|
||||
}
|
||||
|
||||
private bool IsGuardedCrate(Plugin plugin)
|
||||
{
|
||||
return plugin != null && plugin != this && (plugin.Name == "GuardedCrate" || plugin.Name == "NpcSpawn");
|
||||
}
|
||||
|
||||
private void OnGuardedCrateEventStart(UnityEngine.Vector3 at)
|
||||
{
|
||||
MarkHook("OnGuardedCrateEventStart");
|
||||
|
||||
var crate = new GuardedCrateEvent { At = at, Key = _gcStarting };
|
||||
_guardedCrates.Add(crate);
|
||||
|
||||
// One `gcrate position` creates one event, so the key is spent on the first.
|
||||
if (_gcStarting != null)
|
||||
{
|
||||
_gcOpened = crate;
|
||||
_gcStarting = null;
|
||||
}
|
||||
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(GcStartedKind, "event");
|
||||
PutPosition(frame, at);
|
||||
|
||||
if (crate.Key != null)
|
||||
frame["key"] = crate.Key;
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void OnGuardedCrateEventEnded(BasePlayer player, HackableLockedCrate crate)
|
||||
{
|
||||
MarkHook("OnGuardedCrateEventEnded");
|
||||
|
||||
// A guard killed by another NPC, or by nobody, clears the crate for nobody.
|
||||
bool winner = player != null && !player.IsNpc;
|
||||
|
||||
if (winner)
|
||||
TallyFor(player).GuardedWon++;
|
||||
|
||||
GuardedCrateEvent ended = EndedGuardedCrate(player, crate);
|
||||
|
||||
if (ended != null)
|
||||
_guardedCrates.Remove(ended);
|
||||
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(GcEndedKind, "event");
|
||||
|
||||
if (winner)
|
||||
{
|
||||
frame["steamId"] = SteamIdOf(player);
|
||||
frame["name"] = player.displayName;
|
||||
}
|
||||
|
||||
if (ended != null)
|
||||
PutPosition(frame, ended.At);
|
||||
else if (crate != null && !crate.IsDestroyed)
|
||||
PutPosition(frame, crate.transform.position);
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void OnGuardedCrateEventFailed(UnityEngine.Vector3 at)
|
||||
{
|
||||
MarkHook("OnGuardedCrateEventFailed");
|
||||
|
||||
GuardedCrateEvent failed = NearestGuardedCrate(at, 1f);
|
||||
|
||||
if (failed != null)
|
||||
_guardedCrates.Remove(failed);
|
||||
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(GcFailedKind, "event");
|
||||
PutPosition(frame, at);
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread, inside the ended hook. Which running event ended: the one Guarded
|
||||
/// Crate's own list has just marked complete; else the one nearest its crate, else its
|
||||
/// winner; else the only one running. Null when none can be told.
|
||||
/// </summary>
|
||||
private GuardedCrateEvent EndedGuardedCrate(BasePlayer player, HackableLockedCrate crate)
|
||||
{
|
||||
List<KeyValuePair<UnityEngine.Vector3, string>> listed = GuardedCrateList();
|
||||
|
||||
if (listed != null)
|
||||
foreach (KeyValuePair<UnityEngine.Vector3, string> e in listed)
|
||||
if (e.Value == "Complete")
|
||||
{
|
||||
GuardedCrateEvent exact = NearestGuardedCrate(e.Key, 1f);
|
||||
|
||||
if (exact != null)
|
||||
return exact;
|
||||
}
|
||||
|
||||
GuardedCrateEvent near = null;
|
||||
|
||||
if (crate != null && !crate.IsDestroyed)
|
||||
near = NearestGuardedCrate(crate.transform.position, GcEndReach);
|
||||
|
||||
if (near == null && player != null)
|
||||
near = NearestGuardedCrate(player.transform.position, GcEndReach);
|
||||
|
||||
if (near == null && _guardedCrates.Count == 1)
|
||||
near = _guardedCrates[0];
|
||||
|
||||
return near;
|
||||
}
|
||||
|
||||
private GuardedCrateEvent NearestGuardedCrate(UnityEngine.Vector3 at, float reach)
|
||||
{
|
||||
GuardedCrateEvent best = null;
|
||||
float bestDistance = reach;
|
||||
|
||||
foreach (GuardedCrateEvent g in _guardedCrates)
|
||||
{
|
||||
float d = Distance2D(g.At, at);
|
||||
|
||||
if (d <= bestDistance)
|
||||
{
|
||||
bestDistance = d;
|
||||
best = g;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Guarded Crate's own running events, as (position, state) pairs: its
|
||||
/// private <c>EventManager.Instances</c>, read by reflection because it has no API
|
||||
/// that lists them. Null when it is not loaded or its shape has changed, which is said
|
||||
/// once; the hooks alone then keep the set.
|
||||
/// </summary>
|
||||
private List<KeyValuePair<UnityEngine.Vector3, string>> GuardedCrateList()
|
||||
{
|
||||
Plugin gc = GuardedCratePlugin();
|
||||
|
||||
if (gc == null)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
const System.Reflection.BindingFlags Static =
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static;
|
||||
const System.Reflection.BindingFlags Instance =
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
|
||||
|
||||
Type manager = gc.GetType().GetNestedType("EventManager", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
|
||||
System.Reflection.FieldInfo field = manager != null ? manager.GetField("Instances", Static) : null;
|
||||
var list = field != null ? field.GetValue(null) as System.Collections.IList : null;
|
||||
|
||||
if (list == null)
|
||||
throw new InvalidOperationException("no EventManager.Instances");
|
||||
|
||||
var rows = new List<KeyValuePair<UnityEngine.Vector3, string>>();
|
||||
|
||||
foreach (object item in list)
|
||||
{
|
||||
var component = item as UnityEngine.Component;
|
||||
|
||||
// Unity's own null: a destroyed instance still in the list.
|
||||
if (component == null)
|
||||
continue;
|
||||
|
||||
System.Reflection.FieldInfo state = item.GetType().GetField("currentState", Instance);
|
||||
object value = state != null ? state.GetValue(item) : null;
|
||||
rows.Add(new KeyValuePair<UnityEngine.Vector3, string>(component.transform.position, value != null ? value.ToString() : null));
|
||||
}
|
||||
|
||||
_gcListReadable = true;
|
||||
return rows;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_gcListReadable = false;
|
||||
|
||||
if (!_gcListSaid)
|
||||
{
|
||||
_gcListSaid = true;
|
||||
Puts($"could not read Guarded Crate's running events ({Flatten(ex.Message)}); its hooks alone keep the map's guarded crates");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. The running set, replaced by what Guarded Crate's own list says: a
|
||||
/// hotload hears no starts, and its unload and `gcrate stop all` end events without a
|
||||
/// hook. A step's key is kept for an event still running. Nothing changes when the
|
||||
/// list cannot be read.
|
||||
/// </summary>
|
||||
private void SeedGuardedCrates(string why)
|
||||
{
|
||||
if (GuardedCratePlugin() == null)
|
||||
{
|
||||
_guardedCrates.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
List<KeyValuePair<UnityEngine.Vector3, string>> listed = GuardedCrateList();
|
||||
|
||||
if (listed == null)
|
||||
return;
|
||||
|
||||
var running = new List<GuardedCrateEvent>();
|
||||
|
||||
foreach (KeyValuePair<UnityEngine.Vector3, string> e in listed)
|
||||
{
|
||||
if (e.Value != null && e.Value != "Running")
|
||||
continue;
|
||||
|
||||
GuardedCrateEvent known = NearestGuardedCrate(e.Key, 1f);
|
||||
running.Add(new GuardedCrateEvent { At = e.Key, Key = known != null ? known.Key : null });
|
||||
}
|
||||
|
||||
int before = _guardedCrates.Count;
|
||||
_guardedCrates.Clear();
|
||||
_guardedCrates.AddRange(running);
|
||||
|
||||
if (why != null && (running.Count > 0 || before > 0))
|
||||
Puts($"map: {running.Count} guarded crate(s) after {why}");
|
||||
}
|
||||
|
||||
/// <summary>The map's <c>guarded</c> rows, for its world layer.</summary>
|
||||
private void AddGuardedRows(List<object> world)
|
||||
{
|
||||
// `gcrate stop all` ends events without a hook; what its own list no longer has goes.
|
||||
SeedGuardedCrates(null);
|
||||
|
||||
foreach (GuardedCrateEvent g in _guardedCrates)
|
||||
{
|
||||
Dictionary<string, object> row = At(g.At);
|
||||
row["kind"] = "guarded";
|
||||
world.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hello's <c>integrations.guardedCrate</c>: loaded and version, NpcSpawn's beside it
|
||||
/// (D364), and <c>events</c>, the event names in Guarded Crate's data file, absent when
|
||||
/// the file cannot be read.
|
||||
/// </summary>
|
||||
private Dictionary<string, object> GuardedCrateBody()
|
||||
{
|
||||
Plugin gc = GuardedCratePlugin();
|
||||
Dictionary<string, object> body = IntegrationBody(gc);
|
||||
body["npcSpawn"] = IntegrationBody(NpcSpawnPlugin());
|
||||
|
||||
if (gc == null)
|
||||
return body;
|
||||
|
||||
List<string> events = GuardedCrateEventNames();
|
||||
|
||||
if (events != null)
|
||||
body["events"] = events;
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The event names in Guarded Crate's data file (<c>CrateEventEntries</c>, each row's
|
||||
/// <c>event display name)</c>, the key's stray bracket and all), in its order. Null when
|
||||
/// the file is missing or will not parse. Read again only when the file changes: every
|
||||
/// status frame asks. The directory comes from the framework (R19).
|
||||
/// </summary>
|
||||
private List<string> GuardedCrateEventNames()
|
||||
{
|
||||
string path = Path.Combine(Interface.Oxide.DataDirectory, "GuardedCrate.json");
|
||||
DateTime at;
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
at = File.GetLastWriteTimeUtc(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (at == _gcEventNamesAt)
|
||||
return _gcEventNames;
|
||||
|
||||
List<string> names = null;
|
||||
|
||||
try
|
||||
{
|
||||
var data = Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(path));
|
||||
var rows = data["CrateEventEntries"] as Newtonsoft.Json.Linq.JArray;
|
||||
|
||||
if (rows != null)
|
||||
{
|
||||
names = new List<string>();
|
||||
|
||||
foreach (Newtonsoft.Json.Linq.JToken row in rows)
|
||||
{
|
||||
var entry = row as Newtonsoft.Json.Linq.JObject;
|
||||
Newtonsoft.Json.Linq.JToken name = entry != null ? entry["event display name)"] : null;
|
||||
|
||||
if (name != null && name.Type == Newtonsoft.Json.Linq.JTokenType.String && !string.IsNullOrWhiteSpace((string)name))
|
||||
names.Add((string)name);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Puts($"could not read Guarded Crate's data file: {Flatten(ex.Message)}");
|
||||
}
|
||||
|
||||
_gcEventNames = names;
|
||||
_gcEventNamesAt = at;
|
||||
return names;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guarded Crate's console command from its config (<c>command name - (default:
|
||||
/// gcrate)</c>), or <c>gcrate</c> when the file does not say. Empty when the operator
|
||||
/// blanked it, which registers no command at all.
|
||||
/// </summary>
|
||||
private string GuardedCrateCommand()
|
||||
{
|
||||
string path = Path.Combine(Interface.Oxide.ConfigDirectory, "GuardedCrate.json");
|
||||
DateTime at;
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return GcDefaultCommand;
|
||||
|
||||
at = File.GetLastWriteTimeUtc(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return GcDefaultCommand;
|
||||
}
|
||||
|
||||
if (_gcCommand != null && at == _gcCommandAt)
|
||||
return _gcCommand;
|
||||
|
||||
string command = GcDefaultCommand;
|
||||
|
||||
try
|
||||
{
|
||||
var config = Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(path));
|
||||
Newtonsoft.Json.Linq.JToken name = config["command name - (default: gcrate)"];
|
||||
|
||||
if (name != null && name.Type == Newtonsoft.Json.Linq.JTokenType.String)
|
||||
command = ((string)name).Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Puts($"could not read Guarded Crate's config: {Flatten(ex.Message)}");
|
||||
}
|
||||
|
||||
_gcCommand = command;
|
||||
_gcCommandAt = at;
|
||||
return command;
|
||||
}
|
||||
|
||||
private string GcPlacementsPath()
|
||||
{
|
||||
return Path.Combine(Interface.Oxide.DataDirectory, Name, "gc-places.json");
|
||||
}
|
||||
|
||||
private void SaveGcPlacements()
|
||||
{
|
||||
WriteDataFile(GcPlacementsPath(), new List<GcPlacement>(_gcPlacements.Values), "Guarded Crate placements");
|
||||
}
|
||||
|
||||
private void LoadGcPlacements()
|
||||
{
|
||||
_gcPlacements = new Dictionary<string, GcPlacement>(StringComparer.Ordinal);
|
||||
List<GcPlacement> placed = ReadDataFile<GcPlacement>(GcPlacementsPath(), "Guarded Crate placements");
|
||||
|
||||
if (placed != null)
|
||||
foreach (GcPlacement p in placed)
|
||||
if (p != null && !string.IsNullOrEmpty(p.Key))
|
||||
_gcPlacements[p.Key] = p;
|
||||
|
||||
ForgetOldGcPlacements();
|
||||
}
|
||||
|
||||
private void ForgetOldGcPlacements()
|
||||
{
|
||||
long now = NowMs();
|
||||
var stale = new List<string>();
|
||||
|
||||
foreach (KeyValuePair<string, GcPlacement> kv in _gcPlacements)
|
||||
if (now >= kv.Value.AtMs + GcPlaceMemoryMs)
|
||||
stale.Add(kv.Key);
|
||||
|
||||
foreach (string key in stale)
|
||||
_gcPlacements.Remove(key);
|
||||
|
||||
if (stale.Count > 0)
|
||||
SaveGcPlacements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Place one guarded crate for a run's step: <c>{ key, event }</c> and a
|
||||
/// location as a world call takes one (a monument and offsets, or x and z). A key
|
||||
/// already answered is answered again with its first placement and places nothing.
|
||||
/// </summary>
|
||||
private void HandleGcPlace(Dictionary<string, object> cmd, string reqId)
|
||||
{
|
||||
if (!WorldWritesAllowed(reqId))
|
||||
return;
|
||||
|
||||
string key = StringField(cmd, "key");
|
||||
|
||||
if (string.IsNullOrEmpty(key) || key.Length > PaymentKeyMax)
|
||||
{
|
||||
EmitGcError(reqId, "malformed", "a placement is made under a key of at most 128 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
ForgetOldGcPlacements();
|
||||
|
||||
GcPlacement seen;
|
||||
|
||||
if (_gcPlacements.TryGetValue(key, out seen))
|
||||
{
|
||||
EmitGcPlaced(reqId, seen, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string wanted = (StringField(cmd, "event") ?? string.Empty).Trim();
|
||||
|
||||
if (wanted.Length == 0)
|
||||
{
|
||||
EmitGcError(reqId, "bad-event", "a guarded crate is placed as one of Guarded Crate's events, and none was named");
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin gc = GuardedCratePlugin();
|
||||
|
||||
if (gc == null)
|
||||
{
|
||||
EmitGcError(reqId, "gc-missing", "Guarded Crate is not loaded on this server");
|
||||
return;
|
||||
}
|
||||
|
||||
// Guarded Crate's `position` does not check this itself: the crate would land unguarded.
|
||||
if (NpcSpawnPlugin() == null)
|
||||
{
|
||||
EmitGcError(reqId, "npcspawn-missing", "Guarded Crate is loaded but NpcSpawn is missing: its events cannot start");
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> events = GuardedCrateEventNames();
|
||||
string name = null;
|
||||
|
||||
if (events != null)
|
||||
{
|
||||
// As Guarded Crate finds an event: by name, ignoring case.
|
||||
foreach (string e in events)
|
||||
if (string.Equals(e, wanted, StringComparison.OrdinalIgnoreCase))
|
||||
name = e;
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
EmitGcError(reqId, "bad-event", $"'{wanted}' is not one of Guarded Crate's events here ({string.Join(", ", events.ToArray())})");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
name = wanted;
|
||||
|
||||
if (!_consolePlugins)
|
||||
{
|
||||
EmitGcError(reqId, "console-unavailable",
|
||||
"this framework does not run another plugin's console command from code, so a guarded crate cannot be placed here (D363, Rust-Plugins#34)");
|
||||
return;
|
||||
}
|
||||
|
||||
UnityEngine.Vector3 at;
|
||||
string reason, refusal;
|
||||
|
||||
if (!ResolveWorldPoint(cmd, out at, out reason, out refusal))
|
||||
{
|
||||
EmitGcError(reqId, reason, refusal);
|
||||
return;
|
||||
}
|
||||
|
||||
string command = GuardedCrateCommand();
|
||||
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
EmitGcError(reqId, "refused", "Guarded Crate's console command is switched off in its config");
|
||||
return;
|
||||
}
|
||||
|
||||
// Invariant, so a server whose locale writes 1,5 still sends 1.5.
|
||||
string point = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:0.##},{1:0.##},{2:0.##}", at.x, at.y, at.z);
|
||||
string reply = null;
|
||||
_gcOpened = null;
|
||||
_gcStarting = key;
|
||||
|
||||
try
|
||||
{
|
||||
reply = ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), command, "position", name, point);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reply = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gcStarting = null;
|
||||
}
|
||||
|
||||
GuardedCrateEvent opened = _gcOpened;
|
||||
_gcOpened = null;
|
||||
|
||||
if (opened == null)
|
||||
{
|
||||
RefuseGcPlace(reqId, gc, reply);
|
||||
return;
|
||||
}
|
||||
|
||||
var placement = new GcPlacement { Key = key, Event = name, X = opened.At.x, Z = opened.At.z, AtMs = NowMs() };
|
||||
_gcPlacements[key] = placement;
|
||||
SaveGcPlacements();
|
||||
|
||||
_lastWorldSummary = $"placed a {name} guarded crate at {Grid(opened.At) ?? "?"} for key {key}";
|
||||
Puts($"world: {_lastWorldSummary}");
|
||||
EmitGcPlaced(reqId, placement, false);
|
||||
}
|
||||
|
||||
/// <summary>Why our <c>gcrate position</c> placed nothing, told by Guarded Crate's own messages, read from its lang.</summary>
|
||||
private void RefuseGcPlace(string reqId, Plugin gc, string reply)
|
||||
{
|
||||
string said = string.IsNullOrEmpty(reply) ? null : Flatten(reply).Trim();
|
||||
|
||||
if (said != null && said == GcMessage(gc, "EVENT_INTERSECTING"))
|
||||
EmitGcError(reqId, "gc-too-close", "another guarded crate is running within 80 m of that spot");
|
||||
else if (said != null && said == GcMessage(gc, "EVENT_NOT_FOUND"))
|
||||
EmitGcError(reqId, "bad-event", "Guarded Crate has no event of that name");
|
||||
else if (said != null && said == GcMessage(gc, "EVENT_POSITION_INVALID"))
|
||||
EmitGcError(reqId, "bad-position", "Guarded Crate refused that position");
|
||||
else
|
||||
EmitGcError(reqId, "refused", $"Guarded Crate placed nothing{(said != null ? $": {said}" : string.Empty)}");
|
||||
}
|
||||
|
||||
/// <summary>One of Guarded Crate's messages, flattened as a reply is, or null.</summary>
|
||||
private string GcMessage(Plugin gc, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
string text = lang.GetMessage(key, gc);
|
||||
return string.IsNullOrEmpty(text) ? null : Flatten(text).Trim();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitGcPlaced(string reqId, GcPlacement placement, bool repeat)
|
||||
{
|
||||
Dictionary<string, object> frame = Frame("gc.placed", "reply");
|
||||
frame["key"] = placement.Key;
|
||||
frame["event"] = placement.Event;
|
||||
frame["repeat"] = repeat;
|
||||
PutPosition(frame, new UnityEngine.Vector3(placement.X, 0f, placement.Z));
|
||||
|
||||
if (reqId != null)
|
||||
frame["reqId"] = reqId;
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void EmitGcError(string reqId, string reason, string message)
|
||||
{
|
||||
Dictionary<string, object> frame = Frame("gc.error", "reply");
|
||||
frame["reason"] = reason;
|
||||
frame["message"] = message;
|
||||
|
||||
if (reqId != null)
|
||||
frame["reqId"] = reqId;
|
||||
|
||||
_lastWorldSummary = $"Guarded Crate refused ({reason}): {message}";
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
// ---- the zone's messages (D193) ----
|
||||
//
|
||||
// ZoneManager can say a zone's enter and leave messages itself, but only to chat unless its
|
||||
@@ -10683,6 +11387,9 @@ namespace Oxide.Plugins
|
||||
{
|
||||
int zones = 0, crates = 0, npcs = 0;
|
||||
|
||||
// Guarded Crate's `stop all` ends events without a hook; count what is really running.
|
||||
SeedGuardedCrates(null);
|
||||
|
||||
foreach (WorldEntry e in _world.Values)
|
||||
{
|
||||
if (e.Kind == WorldKindZone)
|
||||
@@ -10699,6 +11406,8 @@ namespace Oxide.Plugins
|
||||
$"truePve={(TruePvePlugin() != null)} truePveLeftover={_truePvePending.Count} " +
|
||||
$"dynamicPvp={(DynamicPvpPlugin() != null)} pvpZones={_pvpZones.Count} " +
|
||||
$"dangerousTreasures={(DangerousTreasuresPlugin() != null)} treasures={_treasures.Count} dtStarts={_dtStarts.Count} dtWarmingUp={DtWarmingUp()} " +
|
||||
$"guardedCrate={(GuardedCratePlugin() != null)} npcSpawn={(NpcSpawnPlugin() != null)} guarded={_guardedCrates.Count} gcPlaces={_gcPlacements.Count} " +
|
||||
$"gcEvents={(GuardedCrateEventNames() != null ? string.Join("|", GuardedCrateEventNames().ToArray()) : "none")} gcCommand={GuardedCrateCommand()} gcListReadable={_gcListReadable} " +
|
||||
$"wipeId={(_wipeId ?? "none")} calls={_worldCalls.Count} consolePlugins={_consolePlugins} " +
|
||||
$"credits={_vehicleCredit.Count} chinookCrates={_chinookCrates.Count} said={_worldEventsSaid} " +
|
||||
$"worldEvents={(WorldEventsLive() ? "live" : $"quiet (ready={_worldReady} quitting={Rust.Application.isQuitting} loadingSave={Rust.Application.isLoadingSave})")}" + Environment.NewLine +
|
||||
@@ -14311,7 +15020,9 @@ namespace Oxide.Plugins
|
||||
// Protocol 14 (§14.3.3): whether the map's pvp layer and the feed's zone lines can come.
|
||||
{ "dynamicPvp", IntegrationBody(DynamicPvpPlugin()) },
|
||||
// Protocol 14 (§14.3.4): whether a step may open a Dangerous Treasures event here.
|
||||
{ "dangerousTreasures", IntegrationBody(DangerousTreasuresPlugin()) }
|
||||
{ "dangerousTreasures", IntegrationBody(DangerousTreasuresPlugin()) },
|
||||
// Protocol 14 (§14.3.5): whether a step may place a guarded crate here, and which.
|
||||
{ "guardedCrate", GuardedCrateBody() }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15713,6 +16424,9 @@ namespace Oxide.Plugins
|
||||
// Dangerous Treasures' running chests (§14.3.4): the world's, like a locked crate.
|
||||
AddTreasureRows(world);
|
||||
|
||||
// Guarded Crate's running events (§14.3.5), likewise.
|
||||
AddGuardedRows(world);
|
||||
|
||||
Dictionary<string, object> reply = Frame("map.live", "reply");
|
||||
reply["mapKey"] = MapKey();
|
||||
reply["world"] = world;
|
||||
@@ -16873,6 +17587,15 @@ namespace Oxide.Plugins
|
||||
if (plane == null || plane.IsDestroyed || !_planeAnnounced.Add(plane) || !WorldEventsLive())
|
||||
return;
|
||||
|
||||
// Guarded Crate delivers its crate by cargo plane: that is its event, said as
|
||||
// `gc.started`, not an airdrop (§14.3.5). It has registered the plane by now.
|
||||
Plugin gc = GuardedCratePlugin();
|
||||
|
||||
object ours = gc != null ? gc.Call("API_IsGuardedCrateCargoPlane", plane as CargoPlane) : null;
|
||||
|
||||
if (ours is bool && (bool)ours)
|
||||
return;
|
||||
|
||||
WorldWho who;
|
||||
_planeCaller.TryGetValue(plane, out who);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user