feat(protocol2): house registry board (Part B ph.3)
Overlay BridgeHousing (new): a diff sweep over BaseHouse.AllHouses -> house.update / house.remove (owner, region, location, decay level, co-owners, friends, placement price), complementing the existing house.decay transition feed. HousingSweepSeconds (300s); wired into [bridge reload|sweepnow|status. Stock ServUO has no "for sale" flag, so this is an owner->houses registry; price is the placement value, not a listing. Sidecar: houses board table with upsert/delete/all; main routes house.update/ remove into it; GET /houses served from the store. Docs: INTEGRATION.md house.* events + /houses endpoint; PROTOCOL_2 ph.3 built. Verified: sidecar cargo check clean; overlay compiles in the full ServUO Scripts tree (0 errors, 0 warnings). Live run pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
165
overlay/Scripts/Custom/Bridge/BridgeHousing.cs
Normal file
165
overlay/Scripts/Custom/Bridge/BridgeHousing.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Server.Multis;
|
||||
|
||||
namespace Server.Custom.Bridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class BridgeHousing
|
||||
{
|
||||
private static Timer _timer;
|
||||
|
||||
// house serial -> last-emitted signature.
|
||||
private static readonly Dictionary<Serial, string> _last = new Dictionary<Serial, string>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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.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);
|
||||
}
|
||||
|
||||
/// <summary>Runs one sweep now. Wired into `[bridge sweepnow`.</summary>
|
||||
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<Serial>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user