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:
@@ -296,6 +296,25 @@ Who's online and where. A population snapshot is polled (`PresenceSweepSeconds`,
|
|||||||
"who":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","player":true},"t":...}
|
"who":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","player":true},"t":...}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Houses (Protocol 2.0)
|
||||||
|
|
||||||
|
The house registry — one row per house, complementing the `house.decay` *transition* feed (§ above). Polled (`HousingSweepSeconds`, default 300s) and diffed like the other boards.
|
||||||
|
|
||||||
|
| kind | fields | notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `house.update` | `serial`, `name`, `owner` (actor or null), `coOwners`, `friends`, `region`, `map`, `x`,`y`,`z`, `decay`, `price`, `builtOn`, `lastRefreshed` | A house's owner/region/decay/co-owners changed, or first sight this connection. `decay` is the level name (e.g. `LikeNew`). `price` is the placement value — **stock ServUO has no "for sale" flag**, so this is not a listing. |
|
||||||
|
| `house.remove` | `serial` | The house was demolished or no longer exists. Drop the row. |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"kind":"house.update","serial":"0x40001234","name":"The Silver Anvil","decay":"LikeNew",
|
||||||
|
"price":432100,"map":"Felucca","x":1420,"y":1631,"z":0,"region":"Britain",
|
||||||
|
"owner":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","player":true},
|
||||||
|
"coOwners":2,"friends":5,"builtOn":"2026-01-02T00:00:00Z","lastRefreshed":"2026-07-10T00:00:00Z",
|
||||||
|
"t":1752489280000}
|
||||||
|
```
|
||||||
|
|
||||||
|
Render from `GET /houses` (§6) on connect, then keep live with these events.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. REST — read queries
|
## 5. REST — read queries
|
||||||
@@ -602,6 +621,18 @@ GET /online
|
|||||||
|
|
||||||
The current online population — total plus per-facet and per-region breakdowns. The latest `presence.online` snapshot (from SQLite, so it survives a sidecar restart); keep it live with the `presence.online` stream (§4). `count: 0` with empty maps if the shard hasn't reported yet. For the population time series, `GET /history?kind=presence.online`.
|
The current online population — total plus per-facet and per-region breakdowns. The latest `presence.online` snapshot (from SQLite, so it survives a sidecar restart); keep it live with the `presence.online` stream (§4). `count: 0` with empty maps if the shard hasn't reported yet. For the population time series, `GET /history?kind=presence.online`.
|
||||||
|
|
||||||
|
### House registry (Protocol 2.0)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /houses
|
||||||
|
→ { "houses": [ {"kind":"house.update","serial":"0x40001234","name":"The Silver Anvil",
|
||||||
|
"decay":"LikeNew","price":432100,"map":"Felucca","x":1420,"y":1631,"z":0,"region":"Britain",
|
||||||
|
"owner":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","player":true},
|
||||||
|
"coOwners":2,"friends":5,"builtOn":"...","lastRefreshed":"...","t":...}, ... ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Every house's latest snapshot — owner→houses map. Served from the sidecar's projection, kept current by the `house.*` stream (§4). Ordered by name. Survives a sidecar restart.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Status codes
|
## 7. Status codes
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ If the website mirrors rosters/links (it does — `store.record_link`), it must
|
|||||||
|
|
||||||
1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild` → `guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.*
|
1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild` → `guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.*
|
||||||
2. ~~**Presence.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgePresence.cs`: a `presence.online` sweep (total + per-facet + per-region, emitted on change) and real-time `region.enter` (`EventSink.OnEnterRegion`, player-filtered). `PresenceSweepSeconds` (30s), wired into `[bridge`. `GET /online` serves the latest snapshot from the event store (population series via `/history?kind=presence.online`). *Live run pending.*
|
2. ~~**Presence.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgePresence.cs`: a `presence.online` sweep (total + per-facet + per-region, emitted on change) and real-time `region.enter` (`EventSink.OnEnterRegion`, player-filtered). `PresenceSweepSeconds` (30s), wired into `[bridge`. `GET /online` serves the latest snapshot from the event store (population series via `/history?kind=presence.online`). *Live run pending.*
|
||||||
3. **Housing registry.** Extend the decay sweep to a full owner→houses list → `GET /houses`. (Selected from the §11 menu. Note: stock ServUO has no "for sale" flag on houses, so the registry is owner→houses; for-sale is dropped.)
|
3. ~~**Housing registry.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeHousing.cs`: a house sweep over `BaseHouse.AllHouses` → `house.update`/`house.remove` (owner, region, location, decay, co-owners, friends, price), complementing the existing `house.decay` transition feed. `HousingSweepSeconds` (300s), wired into `[bridge`. Sidecar `houses` board + `GET /houses`. (Stock ServUO has no "for sale" flag, so this is owner→houses; `price` is the placement value, not a listing.) *Live run pending.*
|
||||||
4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`.
|
4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`.
|
||||||
5. **Factions/VvV** — only after confirming which system the shard runs; stream just the enabled one.
|
5. **Factions/VvV** — only after confirming which system the shard runs; stream just the enabled one.
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ namespace Server.Custom.Bridge
|
|||||||
BridgeSocial.Rearm();
|
BridgeSocial.Rearm();
|
||||||
BridgeGovernance.Rearm();
|
BridgeGovernance.Rearm();
|
||||||
BridgePresence.Rearm();
|
BridgePresence.Rearm();
|
||||||
|
BridgeHousing.Rearm();
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
||||||
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
|
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
|
||||||
break;
|
break;
|
||||||
@@ -179,12 +180,14 @@ namespace Server.Custom.Bridge
|
|||||||
BridgeSocial.SweepOnce();
|
BridgeSocial.SweepOnce();
|
||||||
BridgeGovernance.SweepOnce();
|
BridgeGovernance.SweepOnce();
|
||||||
BridgePresence.SweepOnce();
|
BridgePresence.SweepOnce();
|
||||||
|
BridgeHousing.SweepOnce();
|
||||||
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
|
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -198,6 +201,7 @@ namespace Server.Custom.Bridge
|
|||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgeHousing.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
||||||
break;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,25 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// House registry (Protocol 2.0): house.update folds in each house's latest state
|
||||||
|
// (one row per serial); house.remove drops a demolished/traded house.
|
||||||
|
"house.update" => {
|
||||||
|
if let Some(serial) = ev.value.get("serial").and_then(|s| s.as_str()) {
|
||||||
|
if let Err(e) = event_store
|
||||||
|
.upsert_house(serial, ev.value.get("name").and_then(|n| n.as_str()), &text, t)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(error = %e, "failed to upsert house registry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"house.remove" => {
|
||||||
|
if let Some(serial) = ev.value.get("serial").and_then(|s| s.as_str()) {
|
||||||
|
if let Err(e) = event_store.delete_house(serial).await {
|
||||||
|
tracing::warn!(error = %e, "failed to remove house registry row");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,6 +246,40 @@ impl Store {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(parse_json_column(rows))
|
Ok(parse_json_column(rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- house registry (Protocol 2.0) ----
|
||||||
|
|
||||||
|
/// Upserts one house's latest state, keyed by serial. Fed from `house.update`.
|
||||||
|
pub async fn upsert_house(&self, serial: &str, name: Option<&str>, json: &str, t: i64) -> anyhow::Result<()> {
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO houses (serial, name, json, updated_t) VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(serial) DO UPDATE SET name = excluded.name, json = excluded.json, updated_t = excluded.updated_t",
|
||||||
|
)
|
||||||
|
.bind(serial)
|
||||||
|
.bind(name)
|
||||||
|
.bind(json)
|
||||||
|
.bind(t)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops one house from the registry. Fed from `house.remove` (demolished / traded away).
|
||||||
|
pub async fn delete_house(&self, serial: &str) -> anyhow::Result<()> {
|
||||||
|
sqlx::query("DELETE FROM houses WHERE serial = ?")
|
||||||
|
.bind(serial)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full house registry: every house's latest snapshot, ordered by name then serial.
|
||||||
|
pub async fn houses_all(&self) -> anyhow::Result<Vec<Value>> {
|
||||||
|
let rows = sqlx::query("SELECT json FROM houses ORDER BY name, serial")
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(parse_json_column(rows))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_json_column(rows: Vec<sqlx::sqlite::SqliteRow>) -> Vec<Value> {
|
fn parse_json_column(rows: Vec<sqlx::sqlite::SqliteRow>) -> Vec<Value> {
|
||||||
@@ -297,4 +331,11 @@ CREATE TABLE IF NOT EXISTS governors (
|
|||||||
json TEXT NOT NULL,
|
json TEXT NOT NULL,
|
||||||
updated_t INTEGER NOT NULL
|
updated_t INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS houses (
|
||||||
|
serial TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
json TEXT NOT NULL,
|
||||||
|
updated_t INTEGER NOT NULL
|
||||||
|
);
|
||||||
"#;
|
"#;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
|||||||
.route("/guilds", get(guilds))
|
.route("/guilds", get(guilds))
|
||||||
.route("/governors", get(governors))
|
.route("/governors", get(governors))
|
||||||
.route("/online", get(online))
|
.route("/online", get(online))
|
||||||
|
.route("/houses", get(houses))
|
||||||
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -730,6 +731,18 @@ async fn governors(State(st): State<AppState>) -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The house registry: every house's latest snapshot (owner/region/location/decay/value). Served
|
||||||
|
/// from the local board table, so it hydrates without the shard and survives an outage.
|
||||||
|
async fn houses(State(st): State<AppState>) -> impl IntoResponse {
|
||||||
|
match st.store.houses_all().await {
|
||||||
|
Ok(houses) => (StatusCode::OK, Json(json!({"houses": houses}))),
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({"error": e.to_string()})),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The current online population: total plus per-facet and per-region counts. This is the most
|
/// The current online population: total plus per-facet and per-region counts. This is the most
|
||||||
/// recent `presence.online` snapshot from the event store (so it survives a sidecar restart); the
|
/// recent `presence.online` snapshot from the event store (so it survives a sidecar restart); the
|
||||||
/// live `presence.online` stream keeps it current, and `GET /history?kind=presence.online` gives the
|
/// live `presence.online` stream keeps it current, and `GET /history?kind=presence.online` gives the
|
||||||
|
|||||||
Reference in New Issue
Block a user