diff --git a/overlay/Scripts/Custom/Bridge/BridgeBoot.cs b/overlay/Scripts/Custom/Bridge/BridgeBoot.cs
index 88d3992..4af7e27 100644
--- a/overlay/Scripts/Custom/Bridge/BridgeBoot.cs
+++ b/overlay/Scripts/Custom/Bridge/BridgeBoot.cs
@@ -164,6 +164,7 @@ namespace Server.Custom.Bridge
BridgeSocial.Rearm();
BridgeGovernance.Rearm();
BridgePresence.Rearm();
+ BridgeHousing.Rearm();
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
break;
@@ -179,12 +180,14 @@ namespace Server.Custom.Bridge
BridgeSocial.SweepOnce();
BridgeGovernance.SweepOnce();
BridgePresence.SweepOnce();
+ BridgeHousing.SweepOnce();
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
+ e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
break;
default:
@@ -198,6 +201,7 @@ namespace Server.Custom.Bridge
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
+ e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
break;
}
diff --git a/overlay/Scripts/Custom/Bridge/BridgeHousing.cs b/overlay/Scripts/Custom/Bridge/BridgeHousing.cs
new file mode 100644
index 0000000..bca9d1c
--- /dev/null
+++ b/overlay/Scripts/Custom/Bridge/BridgeHousing.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Server.Multis;
+
+namespace Server.Custom.Bridge
+{
+ ///
+ /// The housing registry (docs/PROTOCOL_2.md §11 #9). BridgeSweeps already emits house.decay
+ /// *transitions*; this is the complementary *board*: one row per house with owner, location,
+ /// region, co-owners, value, and current decay level, so the website can render an owner→houses
+ /// map. Like the other Part B boards it is a diff sweep over BaseHouse.AllHouses — emit
+ /// house.update only when a house's signature changes, and house.remove when a house is gone.
+ ///
+ /// Note: stock ServUO has no "for sale" flag on a house (houses are traded, not listed), so the
+ /// registry is owner→houses; `price` is the house's placement value, not a sale listing.
+ ///
+ public static class BridgeHousing
+ {
+ private static Timer _timer;
+
+ // house serial -> last-emitted signature.
+ private static readonly Dictionary _last = new Dictionary();
+
+ private static long _sweeps, _emitted, _removed;
+
+ public static void Initialize()
+ {
+ if (!BridgeConfig.Enabled)
+ return;
+
+ 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.HousingSweepSeconds),
+ TimeSpan.FromSeconds(BridgeConfig.HousingSweepSeconds),
+ HouseSweep);
+ }
+
+ public static void Stop()
+ {
+ if (_timer != null) { _timer.Stop(); _timer = null; }
+ }
+
+ public static string Status()
+ {
+ return String.Format("housing(sweeps={0} emitted={1} removed={2} tracked={3})",
+ _sweeps, _emitted, _removed, _last.Count);
+ }
+
+ /// Runs one sweep now. Wired into `[bridge sweepnow`.
+ public static void SweepOnce()
+ {
+ HouseSweep();
+ }
+
+ private static void HouseSweep()
+ {
+ try
+ {
+ _sweeps++;
+
+ if (!BridgeLink.Connected)
+ return; // nothing is listening; do not fill the queue with perishable snapshots
+
+ var seen = new HashSet();
+
+ foreach (var house in BaseHouse.AllHouses)
+ {
+ if (house == null || house.Deleted)
+ continue;
+
+ seen.Add(house.Serial);
+
+ var level = house.DecayLevel; // computed getter — read once
+ var sig = Signature(house, level);
+
+ string prior;
+ if (_last.TryGetValue(house.Serial, out prior) && prior == sig)
+ continue; // unchanged since last emit
+
+ _last[house.Serial] = sig;
+ BridgeLink.Emit(WriteHouse(house, level));
+ _emitted++;
+ }
+
+ var gone = _last.Keys.Where(k => !seen.Contains(k)).ToList();
+ foreach (var serial in gone)
+ {
+ _last.Remove(serial);
+ BridgeLink.Emit(BridgeJson.Begin("house.remove").Ser("serial", serial).End());
+ _removed++;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("[Bridge] housing sweep threw: {0}", ex.Message);
+ }
+ }
+
+ private static string Signature(BaseHouse house, DecayLevel level)
+ {
+ var ownerSerial = house.Owner == null ? 0 : house.Owner.Serial.Value;
+ var region = house.Region;
+ var regionName = region == null ? "" : (region.Name ?? "");
+ var sign = house.Sign;
+ var name = sign == null ? "" : (sign.GetName() ?? "");
+ var coOwners = house.CoOwners == null ? 0 : house.CoOwners.Count;
+
+ return String.Concat(
+ ownerSerial.ToString(), "|",
+ level.ToString(), "|",
+ regionName, "|",
+ name, "|",
+ coOwners.ToString(), "|",
+ house.Price.ToString());
+ }
+
+ private static string WriteHouse(BaseHouse house, DecayLevel level)
+ {
+ var sb = BridgeJson.Begin("house.update")
+ .Ser("serial", house.Serial)
+ .Str("decay", level.ToString())
+ .Num("price", house.Price)
+ .Str("map", house.Map == null ? null : house.Map.Name)
+ .Num("x", house.X).Num("y", house.Y).Num("z", house.Z);
+
+ var sign = house.Sign;
+ if (sign != null)
+ sb.Str("name", sign.GetName());
+
+ var region = house.Region;
+ if (region != null)
+ sb.Str("region", region.Name);
+
+ sb.Actor("owner", house.Owner);
+
+ sb.Num("coOwners", house.CoOwners == null ? 0 : house.CoOwners.Count);
+ sb.Num("friends", house.Friends == null ? 0 : house.Friends.Count);
+
+ sb.Str("builtOn", house.BuiltOn.ToUniversalTime().ToString("o"));
+ sb.Str("lastRefreshed", house.LastRefreshed.ToUniversalTime().ToString("o"));
+
+ return sb.End();
+ }
+ }
+}