feat(bridge): Dangerous Treasures on protocol 14 (PLAN_REDESIGNS §14.3.4) #38
@@ -119,6 +119,13 @@ HunterZ's `Dynamic PVP` (which needs ZoneManager and TruePVE) is optional too. W
|
||||
PvP zones it opens around a downed helicopter, a Bradley, cargo and the rest reach the site's feed and
|
||||
its map. The bridge only reads them: it never makes, maps or erases one of Dynamic PVP's zones.
|
||||
|
||||
nivex's `Dangerous Treasures` is optional as well. Where it is loaded, its treasure events reach the
|
||||
site's feed, its map and the *Treasure Hunter* title, and an event can open one (`dt.start`) through
|
||||
DT's own `dtevent` console command; DT picks the spot and despawns its own chest. For about ten minutes
|
||||
after DT loads it is still finding its spots, and a start then is answered "not ready yet". A framework
|
||||
that cannot run another plugin's console command from code (`rg.world` says `consolePlugins=False`)
|
||||
cannot open 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`).
|
||||
|
||||
@@ -71,7 +71,10 @@ namespace Oxide.Plugins
|
||||
/// <c>world.zone</c>'s <c>truePve</c> and the <c>integrations.truePve</c> it needs; and
|
||||
/// Dynamic PVP (§20.7, PLAN_REDESIGNS.md §14.3.3): <c>pvp.zone.created</c> and
|
||||
/// <c>pvp.zone.deleted</c>, <c>map.live</c>'s <c>pvp</c> layer and
|
||||
/// <c>integrations.dynamicPvp</c>.
|
||||
/// <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>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 14;
|
||||
@@ -277,7 +280,9 @@ namespace Oxide.Plugins
|
||||
"OnCargoPlaneSignaled", "OnAirdrop", "OnSupplyDropDropped", "OnSupplyDropLanded",
|
||||
"OnDeepSeaOpened", "OnDeepSeaClosed",
|
||||
// Protocol 14, Dynamic PVP's zones (§20.7).
|
||||
"OnCreatedDynamicPVP", "OnDeletedDynamicPVP"
|
||||
"OnCreatedDynamicPVP", "OnDeletedDynamicPVP",
|
||||
// Protocol 14, Dangerous Treasures (§20.8).
|
||||
"OnDangerousEventStarted", "OnDangerousEventWon", "OnDangerousEventEnded"
|
||||
};
|
||||
|
||||
private void Init()
|
||||
@@ -571,6 +576,7 @@ namespace Oxide.Plugins
|
||||
LoadWorld();
|
||||
LoadRewards();
|
||||
LoadWorldCalls();
|
||||
LoadDtStarts();
|
||||
}
|
||||
|
||||
// ---- the outbound queue ----
|
||||
@@ -1069,6 +1075,12 @@ namespace Oxide.Plugins
|
||||
HandleWorldCall(cmd, reqId);
|
||||
break;
|
||||
|
||||
case "dt.start":
|
||||
// Protocol 14 (PROTOCOL.md §20.8, D365). One Dangerous Treasures event, opened
|
||||
// through DT's own console command; DT picks the spot and despawns its own.
|
||||
HandleDtStart(cmd, reqId);
|
||||
break;
|
||||
|
||||
case "tally.open":
|
||||
// Protocol 10 (PLAN.md §29). Who takes part in a run, counted by the game.
|
||||
HandleTallyOpen(cmd, reqId);
|
||||
@@ -1635,6 +1647,8 @@ namespace Oxide.Plugins
|
||||
// The map's sets are seeded here for the same reason: a hotload hears no spawns.
|
||||
SeedMapSets();
|
||||
SeedPvpZones("load");
|
||||
SeedTreasures("load");
|
||||
NoteDtSeen(DangerousTreasuresPlugin() != null);
|
||||
|
||||
// Protocol 14: the console probe (D363) and the world events' sweep. Before hello, which
|
||||
// carries what the probe found.
|
||||
@@ -2875,6 +2889,9 @@ namespace Oxide.Plugins
|
||||
|
||||
/// <summary>Protocol 14 (D356): airdrops this player looted first (Scavenger).</summary>
|
||||
public int DropsLooted;
|
||||
|
||||
/// <summary>Protocol 14 (D375): Dangerous Treasures chests this player emptied (Treasure Hunter).</summary>
|
||||
public int TreasuresWon;
|
||||
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>
|
||||
@@ -2942,7 +2959,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
|
||||
&& BestPvpM <= 0f && BestPveM <= 0f && CratesHacked == 0 && DropsLooted == 0 && TreasuresWon == 0
|
||||
&& Crafted.Count == 0 && Built == 0 && Repaired == 0 && Healed == 0
|
||||
&& Rockets == 0 && Explosives == 0 && Missions == 0 && NpcProfileKills.Count == 0
|
||||
&& !HasBalance;
|
||||
@@ -3047,6 +3064,8 @@ namespace Oxide.Plugins
|
||||
frame["cratesHacked"] = tally.CratesHacked;
|
||||
if (tally.DropsLooted > 0)
|
||||
frame["dropsLooted"] = tally.DropsLooted;
|
||||
if (tally.TreasuresWon > 0)
|
||||
frame["treasuresWon"] = tally.TreasuresWon;
|
||||
if (tally.Crafted.Count > 0)
|
||||
frame["crafted"] = tally.Crafted;
|
||||
if (tally.Built > 0)
|
||||
@@ -5177,7 +5196,7 @@ 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))
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin) || IsDangerousTreasures(plugin))
|
||||
NextTick(ResendHello);
|
||||
|
||||
// Dynamic PVP raises its creates as it opens its zones, which at a load are its
|
||||
@@ -5185,6 +5204,14 @@ namespace Oxide.Plugins
|
||||
if (IsDynamicPvp(plugin))
|
||||
NextTick(() => SeedPvpZones("Dynamic PVP loaded"));
|
||||
|
||||
// Its spawn grid is built from now, so a start that finds no spot is "not ready" for a
|
||||
// while (D365); and any chest a reload kept is found again.
|
||||
if (IsDangerousTreasures(plugin))
|
||||
{
|
||||
NoteDtSeen(true);
|
||||
NextTick(() => SeedTreasures("Dangerous Treasures loaded"));
|
||||
}
|
||||
|
||||
// Economics loaded after players connected: what they hold now is not otherwise known
|
||||
// until it moves (D336).
|
||||
if (IsEconomics(plugin))
|
||||
@@ -5212,13 +5239,20 @@ namespace Oxide.Plugins
|
||||
if (plugin != null && plugin.Name == "BetterChat")
|
||||
_titlesRegistered = false;
|
||||
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin))
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin) || IsDangerousTreasures(plugin))
|
||||
NextTick(ResendHello);
|
||||
|
||||
// Its Unload deleted every zone, each with its hook; whatever is left was missed.
|
||||
if (IsDynamicPvp(plugin))
|
||||
_pvpZones.Clear();
|
||||
|
||||
// Its Unload ended every chest, each with its hook.
|
||||
if (IsDangerousTreasures(plugin))
|
||||
{
|
||||
_treasures.Clear();
|
||||
NoteDtSeen(false);
|
||||
}
|
||||
|
||||
ConfigWriteJob job = _configJob;
|
||||
|
||||
if (job == null || plugin == null)
|
||||
@@ -8943,6 +8977,423 @@ namespace Oxide.Plugins
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ---- Dangerous Treasures (PLAN_REDESIGNS §14.3.4, D365) ----
|
||||
//
|
||||
// nivex's Dangerous Treasures opens a guarded, burning treasure chest somewhere on the map,
|
||||
// with its own NPCs, fire and marker. The bridge says when one opens, is won and ends, for
|
||||
// the feed, the map and the tally (Treasure Hunter), and opens one for a run's step through
|
||||
// DT's own console command. DT's source (3.0.0) shapes it:
|
||||
//
|
||||
// * `OnDangerousEventStarted(Vector3)` is raised INSIDE the call that opens the chest (the
|
||||
// chest's radius setter runs its `Awaken`), so a start our `dtevent` caused is heard
|
||||
// before `ConsoleSystem.Run` returns. That is the key's join, and it is exact: nothing
|
||||
// else can open a chest between the two on the main thread.
|
||||
// * `OnDangerousEventWon(looter, invaders)` when the chest is emptied, then
|
||||
// `OnDangerousEventEnded(Vector3)` as it goes. The win does not say which chest, so it is
|
||||
// placed at the running chest nearest the looter. DT's unload ends every chest, each
|
||||
// with its hook.
|
||||
// * A start that opened nothing is told only by DT's reply, which DT's lang file words. So
|
||||
// "it started" is the hook, never the text; the reply is compared with DT's own "Manual
|
||||
// Event Failed" message, read from its lang, to know a start that found no spot. DT
|
||||
// builds its spawn grid for about ten minutes after it loads and fails every request
|
||||
// meanwhile (§14.2.1), so inside fifteen minutes of DT's load that is `dt-not-ready`
|
||||
// (D365), and after it `dt-no-spot`.
|
||||
// * DT's console command is a config value; this plugin runs its default, `dtevent`. An
|
||||
// operator who renamed it gets the refusal with whatever the console answered.
|
||||
|
||||
private const string DtStartedKind = "dt.started";
|
||||
private const string DtWonKind = "dt.won";
|
||||
private const string DtEndedKind = "dt.ended";
|
||||
|
||||
/// <summary>DT's console command, as its config ships it.</summary>
|
||||
private const string DtCommand = "dtevent";
|
||||
|
||||
/// <summary>How long after DT loads a start that found no spot is "not ready" rather than "no spot" (D365).</summary>
|
||||
private const float DtWarmUpSeconds = 15f * 60f;
|
||||
|
||||
/// <summary>How far from a running chest a winner may stand and still be placed at it.</summary>
|
||||
private const float DtWinReach = 100f;
|
||||
|
||||
/// <summary>The most invaders a win names; the rest are counted in <c>invaderCount</c>.</summary>
|
||||
private const int DtInvadersMax = 32;
|
||||
|
||||
/// <summary>Every DT chest is a large wooden box (`box.wooden.large`), for the seed at load.</summary>
|
||||
private const uint DtChestPrefabId = 2206646561u;
|
||||
|
||||
/// <summary>How long a start's key is remembered: seven days, as a payment's is.</summary>
|
||||
private const long DtStartMemoryMs = 7L * 24L * 60L * 60L * 1000L;
|
||||
|
||||
/// <summary>One running chest: where it is, and the step's key when a step opened it.</summary>
|
||||
private sealed class Treasure
|
||||
{
|
||||
public UnityEngine.Vector3 At;
|
||||
public string Key;
|
||||
}
|
||||
|
||||
/// <summary>The running chests, oldest first. Main thread only.</summary>
|
||||
private readonly List<Treasure> _treasures = new List<Treasure>();
|
||||
|
||||
/// <summary>One start a step asked for, kept for its key as a world call is. Persisted as it is.</summary>
|
||||
private sealed class DtStart
|
||||
{
|
||||
public string Key;
|
||||
public int Level;
|
||||
public float X;
|
||||
public float Z;
|
||||
public long AtMs;
|
||||
}
|
||||
|
||||
private Dictionary<string, DtStart> _dtStarts = new Dictionary<string, DtStart>(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>The key of the `dtevent` this plugin is running now, for the start hook. Null otherwise.</summary>
|
||||
private string _dtStarting;
|
||||
|
||||
/// <summary>The chest the start hook heard while our `dtevent` ran.</summary>
|
||||
private Treasure _dtOpened;
|
||||
|
||||
/// <summary><c>Time.realtimeSinceStartup</c> when this plugin first saw DT loaded; negative while it is not.</summary>
|
||||
private float _dtSeenAt = -1f;
|
||||
|
||||
private Plugin DangerousTreasuresPlugin()
|
||||
{
|
||||
return plugins.Find("DangerousTreasures");
|
||||
}
|
||||
|
||||
private bool IsDangerousTreasures(Plugin plugin)
|
||||
{
|
||||
return plugin != null && plugin != this && plugin.Name == "DangerousTreasures";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. DT's load as this plugin saw it: DT's own, or this plugin's when DT was
|
||||
/// already there, which can only make the warm-up window start late, never early.
|
||||
/// </summary>
|
||||
private void NoteDtSeen(bool loaded)
|
||||
{
|
||||
_dtSeenAt = loaded ? UnityEngine.Time.realtimeSinceStartup : -1f;
|
||||
}
|
||||
|
||||
private bool DtWarmingUp()
|
||||
{
|
||||
return _dtSeenAt >= 0f && UnityEngine.Time.realtimeSinceStartup - _dtSeenAt < DtWarmUpSeconds;
|
||||
}
|
||||
|
||||
private void OnDangerousEventStarted(UnityEngine.Vector3 at)
|
||||
{
|
||||
MarkHook("OnDangerousEventStarted");
|
||||
|
||||
var treasure = new Treasure { At = at, Key = _dtStarting };
|
||||
_treasures.Add(treasure);
|
||||
|
||||
// One `dtevent <level>` opens one chest, so the key is spent on the first.
|
||||
if (_dtStarting != null)
|
||||
{
|
||||
_dtOpened = treasure;
|
||||
_dtStarting = null;
|
||||
}
|
||||
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(DtStartedKind, "event");
|
||||
PutPosition(frame, at);
|
||||
|
||||
if (treasure.Key != null)
|
||||
frame["key"] = treasure.Key;
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void OnDangerousEventWon(BasePlayer looter, List<ulong> invaders)
|
||||
{
|
||||
MarkHook("OnDangerousEventWon");
|
||||
|
||||
if (looter == null || looter.IsNpc)
|
||||
return;
|
||||
|
||||
TallyFor(looter).TreasuresWon++;
|
||||
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Treasure chest = NearestTreasure(looter.transform.position, DtWinReach);
|
||||
Dictionary<string, object> frame = Frame(DtWonKind, "event");
|
||||
frame["steamId"] = SteamIdOf(looter);
|
||||
frame["name"] = looter.displayName;
|
||||
|
||||
var named = new List<string>();
|
||||
int count = 0;
|
||||
|
||||
if (invaders != null)
|
||||
{
|
||||
foreach (ulong id in invaders)
|
||||
{
|
||||
count++;
|
||||
|
||||
if (named.Count < DtInvadersMax)
|
||||
named.Add(id.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
frame["invaders"] = named;
|
||||
frame["invaderCount"] = count;
|
||||
PutPosition(frame, chest != null ? chest.At : looter.transform.position);
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void OnDangerousEventEnded(UnityEngine.Vector3 at)
|
||||
{
|
||||
MarkHook("OnDangerousEventEnded");
|
||||
|
||||
// The chest's own position, the one its start was raised with.
|
||||
Treasure chest = NearestTreasure(at, 1f);
|
||||
|
||||
if (chest != null)
|
||||
_treasures.Remove(chest);
|
||||
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(DtEndedKind, "event");
|
||||
PutPosition(frame, at);
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private Treasure NearestTreasure(UnityEngine.Vector3 at, float reach)
|
||||
{
|
||||
Treasure best = null;
|
||||
float bestDistance = reach;
|
||||
|
||||
foreach (Treasure t in _treasures)
|
||||
{
|
||||
float d = Distance2D(t.At, at);
|
||||
|
||||
if (d <= bestDistance)
|
||||
{
|
||||
bestDistance = d;
|
||||
best = t;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. The running chests, found in the world, replacing what the hooks kept: a
|
||||
/// hotload hears no starts. DT has no API that lists them, so a chest is a large wooden
|
||||
/// box carrying DT's own <c>TreasureChest</c> component.
|
||||
/// </summary>
|
||||
private void SeedTreasures(string why)
|
||||
{
|
||||
_treasures.Clear();
|
||||
|
||||
if (DangerousTreasuresPlugin() == null)
|
||||
return;
|
||||
|
||||
foreach (BaseNetworkable n in BaseNetworkable.serverEntities)
|
||||
{
|
||||
var box = n as StorageContainer;
|
||||
|
||||
if (box == null || box.IsDestroyed || box.prefabID != DtChestPrefabId || box.GetComponent("TreasureChest") == null)
|
||||
continue;
|
||||
|
||||
_treasures.Add(new Treasure { At = box.transform.position });
|
||||
}
|
||||
|
||||
if (_treasures.Count > 0)
|
||||
Puts($"map: {_treasures.Count} Dangerous Treasures chest(s) after {why}");
|
||||
}
|
||||
|
||||
/// <summary>The map's <c>treasure</c> rows, for its world layer.</summary>
|
||||
private void AddTreasureRows(List<object> world)
|
||||
{
|
||||
foreach (Treasure t in _treasures)
|
||||
{
|
||||
Dictionary<string, object> row = At(t.At);
|
||||
row["kind"] = "treasure";
|
||||
world.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private string DtStartsPath()
|
||||
{
|
||||
return Path.Combine(Interface.Oxide.DataDirectory, Name, "dt-starts.json");
|
||||
}
|
||||
|
||||
private void SaveDtStarts()
|
||||
{
|
||||
WriteDataFile(DtStartsPath(), new List<DtStart>(_dtStarts.Values), "Dangerous Treasures starts");
|
||||
}
|
||||
|
||||
private void LoadDtStarts()
|
||||
{
|
||||
_dtStarts = new Dictionary<string, DtStart>(StringComparer.Ordinal);
|
||||
List<DtStart> starts = ReadDataFile<DtStart>(DtStartsPath(), "Dangerous Treasures starts");
|
||||
|
||||
if (starts != null)
|
||||
foreach (DtStart s in starts)
|
||||
if (s != null && !string.IsNullOrEmpty(s.Key))
|
||||
_dtStarts[s.Key] = s;
|
||||
|
||||
ForgetOldDtStarts();
|
||||
}
|
||||
|
||||
private void ForgetOldDtStarts()
|
||||
{
|
||||
long now = NowMs();
|
||||
var stale = new List<string>();
|
||||
|
||||
foreach (KeyValuePair<string, DtStart> kv in _dtStarts)
|
||||
if (now >= kv.Value.AtMs + DtStartMemoryMs)
|
||||
stale.Add(kv.Key);
|
||||
|
||||
foreach (string key in stale)
|
||||
_dtStarts.Remove(key);
|
||||
|
||||
if (stale.Count > 0)
|
||||
SaveDtStarts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Open one Dangerous Treasures event for a run's step: <c>{ key, level }</c>,
|
||||
/// 0 Easy, 1 Medium, 2 Hard; DT picks the spot. A key already answered is answered again
|
||||
/// with its first start and opens nothing, so a retry never opens a second chest.
|
||||
/// </summary>
|
||||
private void HandleDtStart(Dictionary<string, object> cmd, string reqId)
|
||||
{
|
||||
if (!WorldWritesAllowed(reqId))
|
||||
return;
|
||||
|
||||
string key = StringField(cmd, "key");
|
||||
|
||||
if (string.IsNullOrEmpty(key) || key.Length > PaymentKeyMax)
|
||||
{
|
||||
EmitDtError(reqId, "malformed", "a start is made under a key of at most 128 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
ForgetOldDtStarts();
|
||||
|
||||
DtStart seen;
|
||||
|
||||
if (_dtStarts.TryGetValue(key, out seen))
|
||||
{
|
||||
EmitDtOpened(reqId, seen, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Not IntField, which reads anything unparseable as 0, and 0 is Easy.
|
||||
string rawLevel = StringField(cmd, "level");
|
||||
int level;
|
||||
|
||||
if (rawLevel == null || !int.TryParse(rawLevel, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out level) || level < 0 || level > 2)
|
||||
{
|
||||
EmitDtError(reqId, "bad-level", $"a Dangerous Treasures event is level 0 (Easy), 1 (Medium) or 2 (Hard), not '{rawLevel}'");
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin dt = DangerousTreasuresPlugin();
|
||||
|
||||
if (dt == null)
|
||||
{
|
||||
EmitDtError(reqId, "dt-missing", "Dangerous Treasures is not loaded on this server");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_consolePlugins)
|
||||
{
|
||||
EmitDtError(reqId, "console-unavailable",
|
||||
"this framework does not run another plugin's console command from code, so Dangerous Treasures cannot be started here (D363, Rust-Plugins#34)");
|
||||
return;
|
||||
}
|
||||
|
||||
string reply = null;
|
||||
_dtOpened = null;
|
||||
_dtStarting = key;
|
||||
|
||||
try
|
||||
{
|
||||
reply = ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), DtCommand, level);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reply = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dtStarting = null;
|
||||
}
|
||||
|
||||
Treasure opened = _dtOpened;
|
||||
_dtOpened = null;
|
||||
|
||||
if (opened == null)
|
||||
{
|
||||
RefuseDtStart(reqId, dt, reply);
|
||||
return;
|
||||
}
|
||||
|
||||
var start = new DtStart { Key = key, Level = level, X = opened.At.x, Z = opened.At.z, AtMs = NowMs() };
|
||||
_dtStarts[key] = start;
|
||||
SaveDtStarts();
|
||||
|
||||
_lastWorldSummary = $"opened a level {level} Dangerous Treasures event at {Grid(opened.At) ?? "?"} for key {key}";
|
||||
Puts($"world: {_lastWorldSummary}");
|
||||
EmitDtOpened(reqId, start, false);
|
||||
}
|
||||
|
||||
/// <summary>Why our <c>dtevent</c> opened nothing: no spot (yet), or DT's own refusal in its words.</summary>
|
||||
private void RefuseDtStart(string reqId, Plugin dt, string reply)
|
||||
{
|
||||
string said = string.IsNullOrEmpty(reply) ? null : Flatten(reply).Trim();
|
||||
string failed = null;
|
||||
|
||||
try
|
||||
{
|
||||
failed = lang.GetMessage("Manual Event Failed", dt);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// A DT without the message: only an empty reply then reads as "no spot".
|
||||
}
|
||||
|
||||
bool noSpot = said == null || (!string.IsNullOrEmpty(failed) && string.Equals(said, Flatten(failed).Trim(), StringComparison.Ordinal));
|
||||
|
||||
if (!noSpot)
|
||||
EmitDtError(reqId, "refused", $"Dangerous Treasures did not start: {said}");
|
||||
else if (DtWarmingUp())
|
||||
EmitDtError(reqId, "dt-not-ready", "Dangerous Treasures is not ready yet: it spends about ten minutes after it loads finding where events may go");
|
||||
else
|
||||
EmitDtError(reqId, "dt-no-spot", "Dangerous Treasures found no valid spot for an event");
|
||||
}
|
||||
|
||||
private void EmitDtOpened(string reqId, DtStart start, bool repeat)
|
||||
{
|
||||
Dictionary<string, object> frame = Frame("dt.opened", "reply");
|
||||
frame["key"] = start.Key;
|
||||
frame["level"] = start.Level;
|
||||
frame["repeat"] = repeat;
|
||||
PutPosition(frame, new UnityEngine.Vector3(start.X, 0f, start.Z));
|
||||
|
||||
if (reqId != null)
|
||||
frame["reqId"] = reqId;
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void EmitDtError(string reqId, string reason, string message)
|
||||
{
|
||||
Dictionary<string, object> frame = Frame("dt.error", "reply");
|
||||
frame["reason"] = reason;
|
||||
frame["message"] = message;
|
||||
|
||||
if (reqId != null)
|
||||
frame["reqId"] = reqId;
|
||||
|
||||
_lastWorldSummary = $"Dangerous Treasures 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
|
||||
@@ -10247,6 +10698,7 @@ namespace Oxide.Plugins
|
||||
$"zones={zones} crates={crates} npcs={npcs} zoneManager={(ZoneManagerPlugin() != null)} " +
|
||||
$"truePve={(TruePvePlugin() != null)} truePveLeftover={_truePvePending.Count} " +
|
||||
$"dynamicPvp={(DynamicPvpPlugin() != null)} pvpZones={_pvpZones.Count} " +
|
||||
$"dangerousTreasures={(DangerousTreasuresPlugin() != null)} treasures={_treasures.Count} dtStarts={_dtStarts.Count} dtWarmingUp={DtWarmingUp()} " +
|
||||
$"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 +
|
||||
@@ -13857,7 +14309,9 @@ namespace Oxide.Plugins
|
||||
// Protocol 14 (§14.3.2): whether a zone step may change PvP rules here.
|
||||
{ "truePve", IntegrationBody(TruePvePlugin()) },
|
||||
// Protocol 14 (§14.3.3): whether the map's pvp layer and the feed's zone lines can come.
|
||||
{ "dynamicPvp", IntegrationBody(DynamicPvpPlugin()) }
|
||||
{ "dynamicPvp", IntegrationBody(DynamicPvpPlugin()) },
|
||||
// Protocol 14 (§14.3.4): whether a step may open a Dangerous Treasures event here.
|
||||
{ "dangerousTreasures", IntegrationBody(DangerousTreasuresPlugin()) }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15256,6 +15710,9 @@ namespace Oxide.Plugins
|
||||
_mapLiveLastMs = clock.ElapsedMilliseconds;
|
||||
_mapLiveMaxMs = Math.Max(_mapLiveMaxMs, _mapLiveLastMs);
|
||||
|
||||
// Dangerous Treasures' running chests (§14.3.4): the world's, like a locked crate.
|
||||
AddTreasureRows(world);
|
||||
|
||||
Dictionary<string, object> reply = Frame("map.live", "reply");
|
||||
reply["mapKey"] = MapKey();
|
||||
reply["world"] = world;
|
||||
|
||||
Reference in New Issue
Block a user