feat(bridge): protocol 7 — a raid frame that names who lives there #7
@@ -26,9 +26,10 @@
|
||||
# the same change that alters the emitters, exactly as the sidecar bumps
|
||||
# PROTOCOL_VERSION and the module bumps its own constant.
|
||||
#
|
||||
# Current: 6 — the transport, the read path, identity, the permission mirror,
|
||||
# configuration from the site, and first-party clans (docs/rust-link/PROTOCOL.md §12).
|
||||
protocol = 6
|
||||
# Current: 7 — the transport, the read path, identity, the permission mirror,
|
||||
# configuration from the site, first-party clans, and a raid frame that names who
|
||||
# lives there (docs/rust-link/PROTOCOL.md §13).
|
||||
protocol = 7
|
||||
|
||||
# ── Oxide compatibility ──────────────────────────────────────────────────────
|
||||
#
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Oxide.Plugins
|
||||
/// overlay's <c>overlay.toml</c> carries this same number so the installer can refuse
|
||||
/// to pair a sidecar and an overlay that disagree.
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 6;
|
||||
private const int ProtocolVersion = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Loopback reconnects are cheap, so the ceiling is low. A sidecar restart should cost
|
||||
@@ -1426,26 +1426,131 @@ namespace Oxide.Plugins
|
||||
return;
|
||||
}
|
||||
|
||||
BuildingBlock block = entity as BuildingBlock;
|
||||
// Protocol 7 (PROTOCOL.md §13): a raid is the loss of anything a base is made of, not
|
||||
// only a building block. A door is NOT a BuildingBlock — both derive from
|
||||
// StabilityEntity as siblings — so until protocol 7 the most common raid there is, two
|
||||
// charges on a door, reached nobody.
|
||||
string structure = StructureKind(entity);
|
||||
|
||||
if (block == null || block.OwnerID == 0UL)
|
||||
// `OwnerID == 0` is the world's own: a monument's doors and walls. Destroying those is
|
||||
// not a raid on anybody, and emitting them would fill the staff feed with loot runs.
|
||||
if (structure == null || entity.OwnerID == 0UL)
|
||||
return;
|
||||
|
||||
TallyFor(attacker).Structures++;
|
||||
// The tally's meaning is unchanged since protocol 2 — building blocks only — so a
|
||||
// leaderboard column does not quietly change what it counts in the middle of a wipe.
|
||||
if (structure == "block")
|
||||
TallyFor(attacker).Structures++;
|
||||
|
||||
Dictionary<string, object> frame = Frame("entity.destroyed", "event");
|
||||
frame["ownerId"] = block.OwnerID.ToString();
|
||||
frame["prefab"] = block.ShortPrefabName;
|
||||
frame["attackerId"] = attacker.UserIDString;
|
||||
frame["ownerId"] = entity.OwnerID.ToString();
|
||||
frame["prefab"] = entity.ShortPrefabName;
|
||||
frame["structure"] = structure;
|
||||
|
||||
// From `userID`, not `UserIDString`. The string is filled in when a player connects,
|
||||
// loads as a sleeper, or is an engine bot — and at no other time, so a player another
|
||||
// plugin spawned carries none. The website skips the raid alert when the attacker is
|
||||
// authorised on the cupboard (PLAN.md D59), and a null here would quietly defeat that.
|
||||
frame["attackerId"] = ((ulong)attacker.userID).ToString();
|
||||
frame["attackerName"] = attacker.displayName;
|
||||
|
||||
string grid = GridOf(block);
|
||||
string grid = GridOf(entity);
|
||||
if (grid != null)
|
||||
frame["grid"] = grid;
|
||||
|
||||
AddAuthorization(frame, entity);
|
||||
|
||||
EmitFrame(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The most people one <c>entity.destroyed</c> frame names as authorised. A cupboard
|
||||
/// has no hard cap on vanilla, and an unbounded list is an unbounded line on the game
|
||||
/// link; 64 is several full teams, and a frame cut short says so.
|
||||
/// </summary>
|
||||
private const int MaxAuthorizedOnFrame = 64;
|
||||
|
||||
/// <summary>
|
||||
/// Which of the four things a base is made of this entity is, or null for anything else.
|
||||
/// Ordered by how often each dies, which is also cheapest-first.
|
||||
/// </summary>
|
||||
private static string StructureKind(BaseEntity entity)
|
||||
{
|
||||
if (entity is BuildingBlock)
|
||||
return "block";
|
||||
|
||||
// External gates are Doors too, which is right: a gate is a door in a wall.
|
||||
if (entity is Door)
|
||||
return "door";
|
||||
|
||||
if (entity is SimpleBuildingBlock)
|
||||
return "wall";
|
||||
|
||||
if (entity is BuildingPrivlidge)
|
||||
return "cupboard";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Who lives here, by the game's own definition: everyone authorised on the building's
|
||||
/// tool cupboard. Both fields are ABSENT when there is no cupboard, which is a different
|
||||
/// answer from an empty list — "nobody is authorised" and "there is nothing to be
|
||||
/// authorised on" — and the module alerts nobody in either case (PLAN.md D67).
|
||||
///
|
||||
/// <para>
|
||||
/// <c>authorizedPlayers</c> is the set, and nothing else is. The cupboard also holds
|
||||
/// <c>recentGroupMembers</c>, which LOOKS like residents and is not: it counts code-lock
|
||||
/// users towards group upkeep and prunes itself on a timer.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The lookup resolves through the entity's building, which is cheap. An external wall
|
||||
/// has no building and falls back to a sphere query on this thread — acceptable because
|
||||
/// it runs only after every cheap filter above has passed, at the rate walls fall rather
|
||||
/// than the rate this hook fires.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static void AddAuthorization(Dictionary<string, object> frame, BaseEntity entity)
|
||||
{
|
||||
// A cupboard being destroyed still holds its list: OnEntityDeath runs before the kill.
|
||||
BuildingPrivlidge cupboard = entity as BuildingPrivlidge ?? entity.GetBuildingPrivilege();
|
||||
|
||||
if (cupboard == null || cupboard.net == null || cupboard.authorizedPlayers == null)
|
||||
return;
|
||||
|
||||
HashSet<ulong> online = new HashSet<ulong>();
|
||||
|
||||
if (BasePlayer.activePlayerList != null)
|
||||
{
|
||||
foreach (BasePlayer player in BasePlayer.activePlayerList)
|
||||
{
|
||||
if (player != null)
|
||||
online.Add(player.userID);
|
||||
}
|
||||
}
|
||||
|
||||
List<object> authorized = new List<object>();
|
||||
|
||||
foreach (ulong steamId in cupboard.authorizedPlayers)
|
||||
{
|
||||
if (authorized.Count >= MaxAuthorizedOnFrame)
|
||||
{
|
||||
frame["authorizedTruncated"] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
authorized.Add(new Dictionary<string, object>
|
||||
{
|
||||
{ "steamId", steamId.ToString() },
|
||||
{ "online", online.Contains(steamId) },
|
||||
});
|
||||
}
|
||||
|
||||
frame["buildingId"] = cupboard.net.ID.Value.ToString();
|
||||
frame["authorized"] = authorized;
|
||||
}
|
||||
|
||||
private void OnPlayerReported(
|
||||
BasePlayer reporter, string targetName, string targetId,
|
||||
string subject, string message, string type)
|
||||
@@ -1875,6 +1980,23 @@ namespace Oxide.Plugins
|
||||
|
||||
Dictionary<string, object> frame = ClanFrame("clan.disbanded", clan);
|
||||
PutPlayer(frame, "steamId", "name", steamId);
|
||||
|
||||
// Protocol 7: who was in it. The website tells a disbanded clan's members, and by the
|
||||
// time it reads this frame the next board may already have removed the roster it would
|
||||
// otherwise ask — the board is re-sent seconds after this, and after an outage it is
|
||||
// applied before the backlog. The game has deleted the clan from its database by now,
|
||||
// and walked `Members` to drop each one's membership, but it never empties the list
|
||||
// itself — so it is still whole here, bounded by the clan's own member limit.
|
||||
if (clan.Members != null)
|
||||
{
|
||||
List<object> members = new List<object>();
|
||||
|
||||
foreach (ClanMember member in clan.Members)
|
||||
members.Add(member.SteamId.ToString());
|
||||
|
||||
frame["members"] = members;
|
||||
}
|
||||
|
||||
EmitFrame(frame);
|
||||
|
||||
ScheduleClansBoard();
|
||||
|
||||
Reference in New Issue
Block a user