feat(bridge): Guarded Crate on protocol 14 (PLAN_REDESIGNS §14.3.5) #39

Merged
whitlocktech merged 1 commits from feat/guarded-crate into edge 2026-10-15 18:18:45 -05:00
2 changed files with 737 additions and 6 deletions

View File

@@ -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`).

View File

@@ -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);