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
|
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.
|
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
|
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
|
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`).
|
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
|
/// 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>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>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>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int ProtocolVersion = 14;
|
private const int ProtocolVersion = 14;
|
||||||
@@ -272,7 +275,9 @@ namespace Oxide.Plugins
|
|||||||
"OnCargoShipHarborApproach", "OnCargoShipHarborArrived", "OnCargoShipHarborLeave", "OnCargoShipEgress",
|
"OnCargoShipHarborApproach", "OnCargoShipHarborArrived", "OnCargoShipHarborLeave", "OnCargoShipEgress",
|
||||||
"OnHelicopterDropCrate", "OnCrateLanded", "CanHackCrate", "OnCrateHack", "OnCrateHackEnd", "OnLootEntity",
|
"OnHelicopterDropCrate", "OnCrateLanded", "CanHackCrate", "OnCrateHack", "OnCrateHackEnd", "OnLootEntity",
|
||||||
"OnCargoPlaneSignaled", "OnAirdrop", "OnSupplyDropDropped", "OnSupplyDropLanded",
|
"OnCargoPlaneSignaled", "OnAirdrop", "OnSupplyDropDropped", "OnSupplyDropLanded",
|
||||||
"OnDeepSeaOpened", "OnDeepSeaClosed"
|
"OnDeepSeaOpened", "OnDeepSeaClosed",
|
||||||
|
// Protocol 14, Dynamic PVP's zones (§20.7).
|
||||||
|
"OnCreatedDynamicPVP", "OnDeletedDynamicPVP"
|
||||||
};
|
};
|
||||||
|
|
||||||
private void Init()
|
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.
|
// The map's sets are seeded here for the same reason: a hotload hears no spawns.
|
||||||
SeedMapSets();
|
SeedMapSets();
|
||||||
|
SeedPvpZones("load");
|
||||||
|
|
||||||
// Protocol 14: the console probe (D363) and the world events' sweep. Before hello, which
|
// Protocol 14: the console probe (D363) and the world events' sweep. Before hello, which
|
||||||
// carries what the probe found.
|
// 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).
|
// 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;
|
// 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
|
// 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).
|
// 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))
|
if (IsZonePlugin(plugin) || IsRunicNpc(plugin) || IsEconomics(plugin) || IsBackpacks(plugin) || IsTruePve(plugin) || IsDynamicPvp(plugin))
|
||||||
NextTick(ResendHello);
|
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
|
// Economics loaded after players connected: what they hold now is not otherwise known
|
||||||
// until it moves (D336).
|
// until it moves (D336).
|
||||||
if (IsEconomics(plugin))
|
if (IsEconomics(plugin))
|
||||||
@@ -5201,9 +5212,13 @@ namespace Oxide.Plugins
|
|||||||
if (plugin != null && plugin.Name == "BetterChat")
|
if (plugin != null && plugin.Name == "BetterChat")
|
||||||
_titlesRegistered = false;
|
_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);
|
NextTick(ResendHello);
|
||||||
|
|
||||||
|
// Its Unload deleted every zone, each with its hook; whatever is left was missed.
|
||||||
|
if (IsDynamicPvp(plugin))
|
||||||
|
_pvpZones.Clear();
|
||||||
|
|
||||||
ConfigWriteJob job = _configJob;
|
ConfigWriteJob job = _configJob;
|
||||||
|
|
||||||
if (job == null || plugin == null)
|
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) ----
|
// ---- the zone's messages (D193) ----
|
||||||
//
|
//
|
||||||
// ZoneManager can say a zone's enter and leave messages itself, but only to chat unless its
|
// 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)} " +
|
$"protocol={ProtocolVersion} eventsEnabled={(_config != null && _config.EventsEnabled)} " +
|
||||||
$"zones={zones} crates={crates} npcs={npcs} zoneManager={(ZoneManagerPlugin() != null)} " +
|
$"zones={zones} crates={crates} npcs={npcs} zoneManager={(ZoneManagerPlugin() != null)} " +
|
||||||
$"truePve={(TruePvePlugin() != null)} truePveLeftover={_truePvePending.Count} " +
|
$"truePve={(TruePvePlugin() != null)} truePveLeftover={_truePvePending.Count} " +
|
||||||
|
$"dynamicPvp={(DynamicPvpPlugin() != null)} pvpZones={_pvpZones.Count} " +
|
||||||
$"wipeId={(_wipeId ?? "none")} calls={_worldCalls.Count} consolePlugins={_consolePlugins} " +
|
$"wipeId={(_wipeId ?? "none")} calls={_worldCalls.Count} consolePlugins={_consolePlugins} " +
|
||||||
$"credits={_vehicleCredit.Count} chinookCrates={_chinookCrates.Count} said={_worldEventsSaid} " +
|
$"credits={_vehicleCredit.Count} chinookCrates={_chinookCrates.Count} said={_worldEventsSaid} " +
|
||||||
$"worldEvents={(WorldEventsLive() ? "live" : $"quiet (ready={_worldReady} quitting={Rust.Application.isQuitting} loadingSave={Rust.Application.isLoadingSave})")}" + Environment.NewLine +
|
$"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() },
|
{ "economics", EconomicsBody() },
|
||||||
{ "backpacks", BackpacksBody() },
|
{ "backpacks", BackpacksBody() },
|
||||||
// Protocol 14 (§14.3.2): whether a zone step may change PvP rules here.
|
// 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["events"] = events;
|
||||||
reply["players"] = players;
|
reply["players"] = players;
|
||||||
reply["bases"] = bases;
|
reply["bases"] = bases;
|
||||||
|
reply["pvp"] = PvpZoneRows();
|
||||||
reply["tookMs"] = clock.ElapsedMilliseconds;
|
reply["tookMs"] = clock.ElapsedMilliseconds;
|
||||||
|
|
||||||
if (sleepersTruncated)
|
if (sleepersTruncated)
|
||||||
|
|||||||
Reference in New Issue
Block a user