Files
servuo-plugins/overlay/Scripts/Custom/Bridge/BridgeSocial.cs
Claude 0fd6b91f22 docs: move docs to RunicGateway/docs, repoint all references
Extracted docs/ (ADMIN_CONTROLS, INTEGRATION, PLAN, PROTOCOL_2, RESEARCH,
SHARD_PREREQS) into the central RunicGateway/docs repo under link/, with
full commit history preserved via git filter-repo.

The source cites these design docs by section throughout, so every in-repo
reference (C# + Rust comments, Bridge.cfg, and the READMEs) is repointed at
the new docs-repo URL. README references are rendered as markdown links; a
Documentation pointer section is added to the top-level README.

Docs repo: https://gitea.whitlocktech.com/RunicGateway/docs
2026-07-18 00:08:34 -05:00

219 lines
7.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Server.Guilds;
namespace Server.Custom.Bridge
{
/// <summary>
/// The guild stream (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §10.1). Guilds have almost no useful EventSink:
/// EventSink.CreateGuild is only the load-time deserialization factory (Server/World.cs), and
/// leave/disband/leader/alliance changes raise nothing. Only EventSink.JoinGuild is real. So,
/// exactly like <see cref="BridgeChamps"/>, the roster is polled: enumerate BaseGuild.List each
/// tick, fold each guild to a small signature, and emit `guild.update` only when it changes.
/// A guild that vanishes (or disbands — Disbanded == leader gone) leaves via `guild.remove`.
///
/// On top of the board we emit a real-time `guild.join` from EventSink.JoinGuild, so a "so-and-
/// so joined" feed does not wait for the next sweep. A membership change also moves the board
/// signature (member count + serial sum), so a *leave* surfaces as the member count dropping in
/// the next `guild.update`; per-member leave events would need a core tap and are a later
/// refinement (§10.1).
///
/// "Created" is derived sidecar-side from a first-seen id (as champs derive it), rather than a
/// wire event — otherwise a sidecar reconnect, which clears the diff cache and re-emits every
/// guild, would look like every guild being created at once.
/// </summary>
public static class BridgeSocial
{
private static Timer _timer;
// guild id -> last-emitted signature. An id absent here has never been emitted (or the cache
// was cleared on reconnect), so its next sweep counts as a change.
private static readonly Dictionary<int, string> _last = new Dictionary<int, string>();
private static long _sweeps, _emitted, _removed, _joins;
public static void Initialize()
{
if (!BridgeConfig.Enabled)
return;
EventSink.JoinGuild += OnJoinGuild;
EventSink.ServerStarted += OnServerStarted;
}
private static void OnServerStarted()
{
BridgeLink.Connected_Core += OnConnected;
Rearm();
}
private static void OnConnected()
{
_last.Clear();
}
/// <summary>Stops and recreates the timer from current config. Called by `[bridge reload`.</summary>
public static void Rearm()
{
Stop();
_timer = Timer.DelayCall(
TimeSpan.FromSeconds(BridgeConfig.GuildSweepSeconds),
TimeSpan.FromSeconds(BridgeConfig.GuildSweepSeconds),
GuildSweep);
}
public static void Stop()
{
if (_timer != null) { _timer.Stop(); _timer = null; }
}
public static string Status()
{
return String.Format("guilds(sweeps={0} emitted={1} removed={2} joins={3} tracked={4})",
_sweeps, _emitted, _removed, _joins, _last.Count);
}
/// <summary>Runs one sweep now. Wired into `[bridge sweepnow`.</summary>
public static void SweepOnce()
{
GuildSweep();
}
private static void GuildSweep()
{
try
{
_sweeps++;
if (!BridgeLink.Connected)
return; // nothing is listening; do not fill the queue with perishable snapshots
var seen = new HashSet<int>();
foreach (var bg in BaseGuild.List.Values)
{
var g = bg as Guild;
// Skip disbanded guilds (leader gone): they linger in the list until cleaned up,
// and treating them as absent lets the "gone" pass below emit guild.remove.
if (g == null || g.Disbanded)
continue;
seen.Add(g.Id);
var sig = Signature(g);
string prior;
if (_last.TryGetValue(g.Id, out prior) && prior == sig)
continue; // unchanged since last emit
_last[g.Id] = sig;
BridgeLink.Emit(WriteGuild(g));
_emitted++;
}
// Anything tracked last sweep but not seen now has disbanded or been removed.
var gone = _last.Keys.Where(k => !seen.Contains(k)).ToList();
foreach (var id in gone)
{
_last.Remove(id);
BridgeLink.Emit(BridgeJson.Begin("guild.remove").Num("id", id).End());
_removed++;
}
}
catch (Exception ex)
{
Console.WriteLine("[Bridge] guild sweep threw: {0}", ex.Message);
}
}
// The volatile fields that define a meaningful change: name, abbreviation, leader, member
// count, the member set (order-independent serial sum), and alliance.
private static string Signature(Guild g)
{
long memberSum = 0;
int count = 0;
var members = g.Members;
if (members != null)
{
for (int i = 0; i < members.Count; i++)
{
var m = members[i];
if (m == null)
continue;
count++;
unchecked { memberSum += (uint)m.Serial.Value; }
}
}
var leaderSerial = g.Leader == null ? 0 : g.Leader.Serial.Value;
return String.Concat(
g.Name ?? "", "|",
g.Abbreviation ?? "", "|",
leaderSerial.ToString(), "|",
count.ToString(), "|",
memberSum.ToString(), "|",
g.Alliance == null ? "" : (g.AllianceName ?? ""));
}
private static string WriteGuild(Guild g)
{
int online = 0, count = 0;
var members = g.Members;
if (members != null)
{
for (int i = 0; i < members.Count; i++)
{
var m = members[i];
if (m == null)
continue;
count++;
if (m.NetState != null)
online++;
}
}
var sb = BridgeJson.Begin("guild.update")
.Num("id", g.Id)
.Str("name", g.Name)
.Str("abbr", g.Abbreviation)
.Num("members", count)
.Num("online", online)
.Str("alliance", g.Alliance == null ? null : g.AllianceName);
sb.Actor("leader", g.Leader);
return sb.End();
}
// ---- real-time join ----
private static void OnJoinGuild(JoinGuildEventArgs e)
{
try
{
if (e == null || e.Mobile == null)
return;
var g = e.Guild as Guild;
var sb = BridgeJson.Begin("guild.join");
if (g != null)
sb.Num("id", g.Id).Str("name", g.Name).Str("abbr", g.Abbreviation);
sb.Actor("who", e.Mobile);
BridgeLink.Emit(sb.End());
_joins++;
}
catch (Exception ex)
{
Console.WriteLine("[Bridge] guild join handler threw: {0}", ex.Message);
}
}
}
}