using System; using System.Collections.Generic; using System.Linq; using Server.Guilds; namespace Server.Custom.Bridge { /// /// 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 , 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. /// 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 _last = new Dictionary(); 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(); } /// Stops and recreates the timer from current config. Called by `[bridge reload`. 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); } /// Runs one sweep now. Wired into `[bridge sweepnow`. 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(); 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); } } } }