feat: TruePVE in the zone step (PLAN_REDESIGNS §14.3.2, WIP)
Stage 3 of the §14.3 integration plan (D362, D368): world.zone's optional
truePve ({exclude: true} or {ruleset}), all or nothing, unmapped on every
path that erases one of our zones, with truepve.json for erases while
TruePVE is away. Docs and PRs still to come.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -67,7 +67,8 @@ namespace Oxide.Plugins
|
||||
/// world events (§20.5, PLAN_REDESIGNS.md §14.3.1): the <c>world.event</c> kind and its
|
||||
/// 24 types, <c>world.call</c>, hello's <c>console.plugins</c>, and the tally's
|
||||
/// <c>cratesHacked</c> and <c>dropsLooted</c>, where <c>apcKills</c> and
|
||||
/// <c>heliKills</c> are no longer sent.
|
||||
/// <c>heliKills</c> are no longer sent; and TruePVE (§20.6, PLAN_REDESIGNS.md §14.3.2):
|
||||
/// <c>world.zone</c>'s <c>truePve</c> and the <c>integrations.truePve</c> it needs.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 14;
|
||||
@@ -561,6 +562,7 @@ namespace Oxide.Plugins
|
||||
// lease command can arrive before the world has finished loading, and it must find
|
||||
// the holds already in place rather than have them loaded over it afterwards.
|
||||
LoadLeases();
|
||||
LoadTruePvePending();
|
||||
LoadWorld();
|
||||
LoadRewards();
|
||||
LoadWorldCalls();
|
||||
@@ -1621,6 +1623,10 @@ namespace Oxide.Plugins
|
||||
DropWorldFromOtherWipes();
|
||||
RecreateZones("load");
|
||||
|
||||
// Mappings an erase left while TruePVE was away (§14.3.2). When TruePVE loads after
|
||||
// this, its own OnPluginLoaded does it.
|
||||
FlushTruePvePending("load");
|
||||
|
||||
// The map's sets are seeded here for the same reason: a hotload hears no spawns.
|
||||
SeedMapSets();
|
||||
|
||||
@@ -5155,6 +5161,11 @@ namespace Oxide.Plugins
|
||||
if (plugin != null && plugin != this && plugin.Name == "ZoneManager")
|
||||
NextTick(() => RecreateZones("ZoneManager reloaded"));
|
||||
|
||||
// TruePVE has read its table by now (it does in Init), so what an erase left while it
|
||||
// was away can go (§14.3.2).
|
||||
if (IsTruePve(plugin))
|
||||
NextTick(() => FlushTruePvePending("TruePVE loaded"));
|
||||
|
||||
// Hello carries ZoneManager's flag list and both helpers' states, so a change to any
|
||||
// of the four is said again rather than waiting for the next connect (§3.1, §3.2).
|
||||
// Economics too: hello carries its starting balance, which the richest column needs;
|
||||
@@ -8086,6 +8097,13 @@ namespace Oxide.Plugins
|
||||
public int DomeType;
|
||||
public int DomeStack;
|
||||
|
||||
/// <summary>
|
||||
/// The TruePVE mapping this zone was given (PLAN_REDESIGNS §14.3.2): <c>exclude</c> or a
|
||||
/// ruleset's name, or null for none. Kept so the path that erases the zone removes the
|
||||
/// mapping too (D362), and touches nothing else in TruePVE's table.
|
||||
/// </summary>
|
||||
public string TruePve;
|
||||
|
||||
/// <summary>When a zone is erased without being asked, on this host's clock (D96). Zero for an entity.</summary>
|
||||
public long DeadlineMs;
|
||||
|
||||
@@ -8525,6 +8543,192 @@ namespace Oxide.Plugins
|
||||
NextTick(() => RestoreDomes("ZoneDomes started"));
|
||||
}
|
||||
|
||||
// ---- TruePVE in the zone step (PLAN_REDESIGNS §14.3.2, D362, D368) ----
|
||||
//
|
||||
// A zone may ask TruePVE to leave it alone (`exclude`) or to use one of its rulesets. The
|
||||
// §14.2 test found the three facts this is built on:
|
||||
//
|
||||
// * `AddOrUpdateMapping(zoneId, name)` answers false for a ruleset TruePVE does not have,
|
||||
// and nothing lists the names, so the call is the check (D368).
|
||||
// * TruePVE keeps its mappings in its own data file, across its reloads and a restart, and
|
||||
// prunes a mapping whose zone is gone only when the zone id is a number. Ours are `rg-…`,
|
||||
// so the path that erases one of our zones removes its mapping (D362).
|
||||
// * It reads that file in `Init`, so by `OnPluginLoaded` the table is there to change.
|
||||
//
|
||||
// An erase while TruePVE is not loaded keeps the id in `truepve.json` and removes it when
|
||||
// TruePVE comes back. Only ids this plugin mapped are ever removed.
|
||||
|
||||
private const string TruePveExclude = "exclude";
|
||||
|
||||
/// <summary>The longest ruleset name a step may send.</summary>
|
||||
private const int TruePveRulesetMax = 64;
|
||||
|
||||
/// <summary>Zone ids whose mapping is still to remove, because TruePVE was not loaded. Main thread only.</summary>
|
||||
private HashSet<string> _truePvePending = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
private Plugin TruePvePlugin()
|
||||
{
|
||||
return plugins.Find("TruePVE");
|
||||
}
|
||||
|
||||
private bool IsTruePve(Plugin plugin)
|
||||
{
|
||||
return plugin != null && plugin != this && plugin.Name == "TruePVE";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A zone's <c>truePve</c>: <c>{"exclude": true}</c> or <c>{"ruleset": "name"}</c>, exactly one.
|
||||
/// Null with true when the command has none; false with a reason when it is malformed.
|
||||
/// </summary>
|
||||
private static bool ReadTruePve(Dictionary<string, object> cmd, out string mapping, out string refusal)
|
||||
{
|
||||
mapping = null;
|
||||
refusal = null;
|
||||
object raw;
|
||||
|
||||
if (!cmd.TryGetValue("truePve", out raw) || raw == null)
|
||||
return true;
|
||||
|
||||
var o = raw as Newtonsoft.Json.Linq.JObject;
|
||||
Newtonsoft.Json.Linq.JToken exclude = o != null ? o["exclude"] : null;
|
||||
Newtonsoft.Json.Linq.JToken ruleset = o != null ? o["ruleset"] : null;
|
||||
bool hasExclude = exclude != null && exclude.Type != Newtonsoft.Json.Linq.JTokenType.Null;
|
||||
bool hasRuleset = ruleset != null && ruleset.Type != Newtonsoft.Json.Linq.JTokenType.Null;
|
||||
|
||||
if (o == null || hasExclude == hasRuleset)
|
||||
{
|
||||
refusal = "a zone's truePve is {\"exclude\": true} or {\"ruleset\": \"name\"}, exactly one";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasExclude)
|
||||
{
|
||||
if (exclude.Type != Newtonsoft.Json.Linq.JTokenType.Boolean || !(bool)exclude)
|
||||
{
|
||||
refusal = "a zone's truePve exclude is true, or left out";
|
||||
return false;
|
||||
}
|
||||
|
||||
mapping = TruePveExclude;
|
||||
return true;
|
||||
}
|
||||
|
||||
string name = ruleset.Type == Newtonsoft.Json.Linq.JTokenType.String ? ((string)ruleset).Trim() : null;
|
||||
|
||||
if (string.IsNullOrEmpty(name) || name.Length > TruePveRulesetMax)
|
||||
{
|
||||
refusal = $"a TruePVE ruleset is named by 1 to {TruePveRulesetMax} characters";
|
||||
return false;
|
||||
}
|
||||
|
||||
mapping = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Removes the TruePVE mapping of a zone this plugin is erasing, if it gave the
|
||||
/// zone one. Called before the zone goes, on every path that erases one (D362).
|
||||
/// </summary>
|
||||
private void UnmapTruePve(WorldEntry e)
|
||||
{
|
||||
if (e == null || e.Kind != WorldKindZone || string.IsNullOrEmpty(e.TruePve))
|
||||
return;
|
||||
|
||||
Plugin tp = TruePvePlugin();
|
||||
|
||||
if (tp != null)
|
||||
{
|
||||
// False is "not in the table", which is what this was for.
|
||||
tp.Call("RemoveMapping", e.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_truePvePending.Add(e.Id))
|
||||
{
|
||||
SaveTruePvePending();
|
||||
Puts($"world: TruePVE is not loaded; {e.Id}'s mapping is removed when it is");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Main thread. Removes every mapping an erase left while TruePVE was away.</summary>
|
||||
private void FlushTruePvePending(string why)
|
||||
{
|
||||
Plugin tp = TruePvePlugin();
|
||||
|
||||
if (tp == null || _truePvePending.Count == 0)
|
||||
return;
|
||||
|
||||
int removed = 0;
|
||||
|
||||
foreach (string id in _truePvePending)
|
||||
{
|
||||
object gone = tp.Call("RemoveMapping", id);
|
||||
|
||||
if (gone is bool && (bool)gone)
|
||||
removed++;
|
||||
}
|
||||
|
||||
int count = _truePvePending.Count;
|
||||
_truePvePending.Clear();
|
||||
SaveTruePvePending();
|
||||
|
||||
_lastWorldSummary = $"TruePVE after {why}: {removed} of {count} left-over mapping(s) removed";
|
||||
Puts($"world: {_lastWorldSummary}");
|
||||
}
|
||||
|
||||
private string TruePvePendingPath()
|
||||
{
|
||||
return Path.Combine(Interface.Oxide.DataDirectory, Name, "truepve.json");
|
||||
}
|
||||
|
||||
private void SaveTruePvePending()
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = TruePvePendingPath();
|
||||
|
||||
if (_truePvePending.Count == 0)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
File.WriteAllText(path, JsonConvert.SerializeObject(new List<string>(_truePvePending), Formatting.Indented), Utf8NoBom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Puts($"could not save the TruePVE leftovers: {Flatten(ex.Message)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadTruePvePending()
|
||||
{
|
||||
_truePvePending = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
try
|
||||
{
|
||||
string path = TruePvePendingPath();
|
||||
|
||||
if (!File.Exists(path))
|
||||
return;
|
||||
|
||||
List<string> ids = JsonConvert.DeserializeObject<List<string>>(File.ReadAllText(path));
|
||||
|
||||
foreach (string id in ids ?? new List<string>())
|
||||
{
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
_truePvePending.Add(id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Puts($"could not read the TruePVE leftovers: {Flatten(ex.Message)}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- the zone's messages (D193) ----
|
||||
//
|
||||
// ZoneManager can say a zone's enter and leave messages itself, but only to chat unless its
|
||||
@@ -9020,6 +9224,22 @@ namespace Oxide.Plugins
|
||||
dome = true;
|
||||
}
|
||||
|
||||
// §14.3.2 (D368): TruePVE's rules in the zone, as `exclude` or one ruleset by name.
|
||||
string truePve;
|
||||
string truePveRefusal;
|
||||
|
||||
if (!ReadTruePve(cmd, out truePve, out truePveRefusal))
|
||||
{
|
||||
EmitWorldError(reqId, "malformed", truePveRefusal);
|
||||
return;
|
||||
}
|
||||
|
||||
if (truePve != null && TruePvePlugin() == null)
|
||||
{
|
||||
EmitWorldError(reqId, "truepve-missing", "TruePVE is not loaded on this server, so a zone cannot change its PvP rules");
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new WorldEntry
|
||||
{
|
||||
Id = $"rg-{ShortRun(runId)}-{NowMs() % 100000000L}-{++_zoneSerial}",
|
||||
@@ -9061,11 +9281,35 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
// The mapping last, all or nothing like the dome: only TruePVE's own answer can say
|
||||
// whether it has a ruleset of that name (D368), and a zone without the rules its
|
||||
// step asked for is not opened at all.
|
||||
if (truePve != null)
|
||||
{
|
||||
object mapped = TruePvePlugin()?.Call("AddOrUpdateMapping", entry.Id, truePve);
|
||||
|
||||
if (!(mapped is bool && (bool)mapped))
|
||||
{
|
||||
RemoveDome(entry);
|
||||
zm.Call("EraseTemporaryZone", this, entry.Id);
|
||||
|
||||
if (truePve == TruePveExclude)
|
||||
EmitWorldError(reqId, "refused", "TruePVE did not map the zone, so it was not opened");
|
||||
else
|
||||
EmitWorldError(reqId, "truepve-ruleset",
|
||||
$"TruePVE on this server has no ruleset named '{truePve}' (names are case-sensitive), so the zone was not opened");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
entry.TruePve = truePve;
|
||||
}
|
||||
|
||||
_world[entry.Id] = entry;
|
||||
SaveWorld();
|
||||
EnsureWorldTimer();
|
||||
|
||||
_lastWorldSummary = $"zone {entry.Id} r={radius:0} for {holdMs / 60000L} min, {flags.Count} flag(s){(dome ? ", domed" : "")}, run {runId}";
|
||||
_lastWorldSummary = $"zone {entry.Id} r={radius:0} for {holdMs / 60000L} min, {flags.Count} flag(s){(dome ? ", domed" : "")}{(truePve != null ? $", TruePVE {truePve}" : "")}, run {runId}";
|
||||
Puts($"world: {_lastWorldSummary}");
|
||||
EmitWorldPlaced(reqId, new List<WorldEntry> { entry }, false);
|
||||
}
|
||||
@@ -9416,6 +9660,9 @@ namespace Oxide.Plugins
|
||||
// left there outlives the zone it was drawn over (D194).
|
||||
RemoveDome(e);
|
||||
|
||||
// And its TruePVE mapping, which TruePVE never prunes for an id like ours (D362).
|
||||
UnmapTruePve(e);
|
||||
|
||||
if (wasThere)
|
||||
zm.Call("EraseTemporaryZone", this, e.Id);
|
||||
|
||||
@@ -9493,7 +9740,11 @@ namespace Oxide.Plugins
|
||||
if (pruned != null)
|
||||
{
|
||||
foreach (string id in pruned)
|
||||
{
|
||||
// A zone ZoneManager could not put back is erased all the same, mapping and all.
|
||||
UnmapTruePve(_world[id]);
|
||||
_world.Remove(id);
|
||||
}
|
||||
|
||||
SaveWorld();
|
||||
}
|
||||
@@ -9700,6 +9951,7 @@ namespace Oxide.Plugins
|
||||
foreach (WorldEntry e in due)
|
||||
{
|
||||
RemoveDome(e);
|
||||
UnmapTruePve(e);
|
||||
|
||||
if (zm != null && zm.Call("CheckZoneID", e.Id) != null)
|
||||
zm.Call("EraseTemporaryZone", this, e.Id);
|
||||
@@ -9748,8 +10000,12 @@ namespace Oxide.Plugins
|
||||
if (stale.Count == 0)
|
||||
return;
|
||||
|
||||
// TruePVE's table outlives a wipe, so the old map's mappings are removed with it.
|
||||
foreach (string id in stale)
|
||||
{
|
||||
UnmapTruePve(_world[id]);
|
||||
_world.Remove(id);
|
||||
}
|
||||
|
||||
SaveWorld();
|
||||
_lastWorldSummary = $"a new wipe dropped {stale.Count} things from the old map";
|
||||
@@ -9775,6 +10031,7 @@ namespace Oxide.Plugins
|
||||
arg.ReplyWith(
|
||||
$"protocol={ProtocolVersion} eventsEnabled={(_config != null && _config.EventsEnabled)} " +
|
||||
$"zones={zones} crates={crates} npcs={npcs} zoneManager={(ZoneManagerPlugin() != null)} " +
|
||||
$"truePve={(TruePvePlugin() != null)} truePveLeftover={_truePvePending.Count} " +
|
||||
$"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 +
|
||||
@@ -13381,7 +13638,9 @@ namespace Oxide.Plugins
|
||||
{ "zoneDomes", ZoneDomesBody() },
|
||||
{ "runicNpc", RunicNpcBody() },
|
||||
{ "economics", EconomicsBody() },
|
||||
{ "backpacks", BackpacksBody() }
|
||||
{ "backpacks", BackpacksBody() },
|
||||
// Protocol 14 (§14.3.2): whether a zone step may change PvP rules here.
|
||||
{ "truePve", IntegrationBody(TruePvePlugin()) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user