feat(bridge): Dynamic PVP's zones on protocol 14 (PLAN_REDESIGNS §14.3.3) #37
@@ -115,6 +115,10 @@ protection inside it, or use one of the operator's own TruePVE rulesets by name;
|
||||
zone step says "TruePVE is not installed on this server". The bridge only ever adds and removes the
|
||||
mappings of zones it opened, and removes each one with its zone, even when TruePVE was away at the time.
|
||||
|
||||
HunterZ's `Dynamic PVP` (which needs ZoneManager and TruePVE) is optional too. Where it is loaded, the
|
||||
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.
|
||||
|
||||
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`).
|
||||
|
||||
@@ -68,7 +68,10 @@ namespace Oxide.Plugins
|
||||
/// 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; 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.
|
||||
/// <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>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 14;
|
||||
@@ -272,7 +275,9 @@ namespace Oxide.Plugins
|
||||
"OnCargoShipHarborApproach", "OnCargoShipHarborArrived", "OnCargoShipHarborLeave", "OnCargoShipEgress",
|
||||
"OnHelicopterDropCrate", "OnCrateLanded", "CanHackCrate", "OnCrateHack", "OnCrateHackEnd", "OnLootEntity",
|
||||
"OnCargoPlaneSignaled", "OnAirdrop", "OnSupplyDropDropped", "OnSupplyDropLanded",
|
||||
"OnDeepSeaOpened", "OnDeepSeaClosed"
|
||||
"OnDeepSeaOpened", "OnDeepSeaClosed",
|
||||
// Protocol 14, Dynamic PVP's zones (§20.7).
|
||||
"OnCreatedDynamicPVP", "OnDeletedDynamicPVP"
|
||||
};
|
||||
|
||||
private void Init()
|
||||
@@ -1629,6 +1634,7 @@ namespace Oxide.Plugins
|
||||
|
||||
// The map's sets are seeded here for the same reason: a hotload hears no spawns.
|
||||
SeedMapSets();
|
||||
SeedPvpZones("load");
|
||||
|
||||
// Protocol 14: the console probe (D363) and the world events' sweep. Before hello, which
|
||||
// carries what the probe found.
|
||||
@@ -5170,10 +5176,15 @@ namespace Oxide.Plugins
|
||||
// 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;
|
||||
// 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).
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin))
|
||||
// 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))
|
||||
NextTick(ResendHello);
|
||||
|
||||
// Dynamic PVP raises its creates as it opens its zones, which at a load are its
|
||||
// monuments'; seeding once it has finished starting catches any it opened before us.
|
||||
if (IsDynamicPvp(plugin))
|
||||
NextTick(() => SeedPvpZones("Dynamic PVP loaded"));
|
||||
|
||||
// Economics loaded after players connected: what they hold now is not otherwise known
|
||||
// until it moves (D336).
|
||||
if (IsEconomics(plugin))
|
||||
@@ -5201,9 +5212,13 @@ namespace Oxide.Plugins
|
||||
if (plugin != null && plugin.Name == "BetterChat")
|
||||
_titlesRegistered = false;
|
||||
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin))
|
||||
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin))
|
||||
NextTick(ResendHello);
|
||||
|
||||
// Its Unload deleted every zone, each with its hook; whatever is left was missed.
|
||||
if (IsDynamicPvp(plugin))
|
||||
_pvpZones.Clear();
|
||||
|
||||
ConfigWriteJob job = _configJob;
|
||||
|
||||
if (job == null || plugin == null)
|
||||
@@ -8730,6 +8745,204 @@ namespace Oxide.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Dynamic PVP's zones (PLAN_REDESIGNS §14.3.3) ----
|
||||
//
|
||||
// HunterZ's Dynamic PVP opens its own PvP zones around a downed helicopter, a Bradley, a
|
||||
// locked crate, an airdrop, cargo and the rest. They are its own: the bridge never makes,
|
||||
// maps or erases one, and only says that they are there, for the feed and the map. The
|
||||
// §14.2 test and the plugin's own source (5.2.0) shape it:
|
||||
//
|
||||
// * `OnCreatedDynamicPVP(zoneId, eventName, position, duration)` and
|
||||
// `OnDeletedDynamicPVP(zoneId, eventName)` are raised for every zone, a reload's and an
|
||||
// unload's included. Its ids are always numeric, so never one of our `rg-…`.
|
||||
// * Neither hook carries the zone's size, and a zone is a sphere (`Radius`) or a cube
|
||||
// (`Size`, cargo's), so both are read from ZoneManager. Cargo's zone is parented to the
|
||||
// ship, so where a zone is comes from its transform, never from where it was made.
|
||||
// * `AllDynamicPVPZones()` and `GetEventName(zoneId)` say which zones are live, so the
|
||||
// set is seeded from Dynamic PVP itself at load: a hotload of the bridge hears no creates.
|
||||
// * `OnPlayerEnterPVP` / `ExitPVP` are not subscribed: they would say who stands where.
|
||||
|
||||
private const string PvpZoneCreatedKind = "pvp.zone.created";
|
||||
private const string PvpZoneDeletedKind = "pvp.zone.deleted";
|
||||
|
||||
/// <summary>One of Dynamic PVP's live zones: its event's name, and how long it was given.</summary>
|
||||
private sealed class PvpZone
|
||||
{
|
||||
public string Event;
|
||||
|
||||
/// <summary>Seconds, or -1 for "as long as its event" (cargo, a monument's).</summary>
|
||||
public int DurationSec;
|
||||
}
|
||||
|
||||
/// <summary>Dynamic PVP's live zones, by its zone id. Main thread only.</summary>
|
||||
private readonly Dictionary<string, PvpZone> _pvpZones = new Dictionary<string, PvpZone>(StringComparer.Ordinal);
|
||||
|
||||
private Plugin DynamicPvpPlugin()
|
||||
{
|
||||
return plugins.Find("DynamicPVP");
|
||||
}
|
||||
|
||||
private bool IsDynamicPvp(Plugin plugin)
|
||||
{
|
||||
return plugin != null && plugin != this && plugin.Name == "DynamicPVP";
|
||||
}
|
||||
|
||||
private void OnCreatedDynamicPVP(string zoneId, string eventName, UnityEngine.Vector3 position, float duration)
|
||||
{
|
||||
MarkHook("OnCreatedDynamicPVP");
|
||||
|
||||
if (string.IsNullOrEmpty(zoneId))
|
||||
return;
|
||||
|
||||
var zone = new PvpZone { Event = eventName ?? "", DurationSec = duration > 0f ? (int)Math.Round(duration) : -1 };
|
||||
_pvpZones[zoneId] = zone;
|
||||
|
||||
// A boot or a reload of Dynamic PVP opens every monument's zone at once, and that is
|
||||
// not news; the map has them all the same.
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(PvpZoneCreatedKind, "event");
|
||||
frame["zoneId"] = zoneId;
|
||||
frame["event"] = zone.Event;
|
||||
|
||||
UnityEngine.Vector3 at;
|
||||
float radius;
|
||||
string shape;
|
||||
|
||||
if (!PvpZoneShape(zoneId, out at, out radius, out shape))
|
||||
at = position;
|
||||
|
||||
PutPosition(frame, at);
|
||||
frame["radius"] = Math.Round(radius, 1);
|
||||
frame["shape"] = shape;
|
||||
frame["durationSec"] = zone.DurationSec;
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
private void OnDeletedDynamicPVP(string zoneId, string eventName)
|
||||
{
|
||||
MarkHook("OnDeletedDynamicPVP");
|
||||
|
||||
if (string.IsNullOrEmpty(zoneId) || !_pvpZones.Remove(zoneId))
|
||||
return;
|
||||
|
||||
// A shutdown deletes every zone, and that is not news. Dynamic PVP's own unload does too,
|
||||
// and is said: the zones really have closed, and the line is staff's.
|
||||
if (!WorldEventsLive())
|
||||
return;
|
||||
|
||||
Dictionary<string, object> frame = Frame(PvpZoneDeletedKind, "event");
|
||||
frame["zoneId"] = zoneId;
|
||||
frame["event"] = eventName ?? "";
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread. Where one of Dynamic PVP's zones is now, and how big: a sphere's radius,
|
||||
/// or for a cube half its longer side across the ground, so the map can draw a ring.
|
||||
/// False when ZoneManager does not hold it.
|
||||
/// </summary>
|
||||
private bool PvpZoneShape(string zoneId, out UnityEngine.Vector3 at, out float radius, out string shape)
|
||||
{
|
||||
at = UnityEngine.Vector3.zero;
|
||||
radius = 0f;
|
||||
shape = "sphere";
|
||||
|
||||
Plugin zm = ZoneManagerPlugin();
|
||||
var zone = zm != null ? zm.Call("GetZoneByID", zoneId) as UnityEngine.Component : null;
|
||||
|
||||
if (zone == null || zone.transform == null)
|
||||
return false;
|
||||
|
||||
at = zone.transform.position;
|
||||
object r = zm.Call("GetZoneRadius", zoneId);
|
||||
|
||||
if (r is float && (float)r > 0f)
|
||||
{
|
||||
radius = (float)r;
|
||||
return true;
|
||||
}
|
||||
|
||||
object s = zm.Call("GetZoneSize", zoneId);
|
||||
|
||||
if (s is UnityEngine.Vector3)
|
||||
{
|
||||
var size = (UnityEngine.Vector3)s;
|
||||
radius = Math.Max(size.x, size.z) / 2f;
|
||||
shape = "cube";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Main thread. The live zones from Dynamic PVP itself, replacing what the hooks kept.</summary>
|
||||
private void SeedPvpZones(string why)
|
||||
{
|
||||
_pvpZones.Clear();
|
||||
Plugin dpvp = DynamicPvpPlugin();
|
||||
|
||||
if (dpvp == null)
|
||||
return;
|
||||
|
||||
var ids = dpvp.Call("AllDynamicPVPZones") as string[];
|
||||
|
||||
if (ids == null)
|
||||
return;
|
||||
|
||||
foreach (string id in ids)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
continue;
|
||||
|
||||
// How long it was given is not asked for; the map does not need it.
|
||||
_pvpZones[id] = new PvpZone { Event = dpvp.Call("GetEventName", id) as string ?? "", DurationSec = -1 };
|
||||
}
|
||||
|
||||
if (_pvpZones.Count > 0)
|
||||
Puts($"map: {_pvpZones.Count} Dynamic PVP zone(s) after {why}");
|
||||
}
|
||||
|
||||
/// <summary>Main thread. The map's <c>pvp</c> layer: every live zone ZoneManager still holds, where it is now.</summary>
|
||||
private List<object> PvpZoneRows()
|
||||
{
|
||||
var rows = new List<object>();
|
||||
List<string> gone = null;
|
||||
|
||||
foreach (KeyValuePair<string, PvpZone> z in _pvpZones)
|
||||
{
|
||||
UnityEngine.Vector3 at;
|
||||
float radius;
|
||||
string shape;
|
||||
|
||||
if (!PvpZoneShape(z.Key, out at, out radius, out shape))
|
||||
{
|
||||
// Erased without its hook (ZoneManager reloaded); Dynamic PVP's own state is
|
||||
// its business, and the map shows only what is there.
|
||||
if (gone == null)
|
||||
gone = new List<string>();
|
||||
|
||||
gone.Add(z.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, object> row = At(at);
|
||||
row["zoneId"] = z.Key;
|
||||
row["event"] = z.Value.Event;
|
||||
row["radius"] = Math.Round(radius, 1);
|
||||
row["shape"] = shape;
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
if (gone != null)
|
||||
{
|
||||
foreach (string id in gone)
|
||||
_pvpZones.Remove(id);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ---- the zone's messages (D193) ----
|
||||
//
|
||||
// ZoneManager can say a zone's enter and leave messages itself, but only to chat unless its
|
||||
@@ -10033,6 +10246,7 @@ namespace Oxide.Plugins
|
||||
$"protocol={ProtocolVersion} eventsEnabled={(_config != null && _config.EventsEnabled)} " +
|
||||
$"zones={zones} crates={crates} npcs={npcs} zoneManager={(ZoneManagerPlugin() != null)} " +
|
||||
$"truePve={(TruePvePlugin() != null)} truePveLeftover={_truePvePending.Count} " +
|
||||
$"dynamicPvp={(DynamicPvpPlugin() != null)} pvpZones={_pvpZones.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 +
|
||||
@@ -13641,7 +13855,9 @@ namespace Oxide.Plugins
|
||||
{ "economics", EconomicsBody() },
|
||||
{ "backpacks", BackpacksBody() },
|
||||
// Protocol 14 (§14.3.2): whether a zone step may change PvP rules here.
|
||||
{ "truePve", IntegrationBody(TruePvePlugin()) }
|
||||
{ "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()) }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15046,6 +15262,7 @@ namespace Oxide.Plugins
|
||||
reply["events"] = events;
|
||||
reply["players"] = players;
|
||||
reply["bases"] = bases;
|
||||
reply["pvp"] = PvpZoneRows();
|
||||
reply["tookMs"] = clock.ElapsedMilliseconds;
|
||||
|
||||
if (sleepersTruncated)
|
||||
|
||||
Reference in New Issue
Block a user