Phase 3: polled sweeps (vitals, house decay, economy)
BridgeSweeps runs three repeating Core-thread timers for the state that has no
EventSink. Cost measured in Phase 1 is why they can run on the main thread: a
full pass of all three is well under a millisecond at the seeded scale.
- Vitals: online players only. Small and volatile; the sidecar diffs snapshots.
Offline characters do not move, so they are served on demand as full profiles
instead, not swept.
- House decay: emits only on a level transition. A silent baseline on
ServerStarted records every house's current stage, so a restart does not
re-announce them. Payload carries from/to, coords, nested ban location,
region, sign name, owner serial+account, and built/refreshed timestamps, all
null-guarded.
- Economy supply: periodic sum of every account's currency as a snapshot. The
level; AccountGoldChange and the vendor events are the flow.
All three re-arm on `[bridge reload`; `[bridge sweepnow` runs one of each on
demand; `[bridge status` reports sweep counters. Sweeps skip emitting while the
sidecar is disconnected, since their state is perishable and re-emitted next
tick anyway (unlike events, which queue through an outage).
Verified on the seeded world with 8s intervals: baseline recorded 29 houses
silently, a probe bumped one Somewhat->Fairly, and the next sweep emitted exactly
one house.decay and none for the other 28. Economy emitted a supply snapshot per
interval. Vitals emitted nothing, correctly, since all seeded characters are
offline. Evidence in docs/PLAN.md §13.
Adds tools/scaffolding/BridgeSweepProbe.cs (never deployed) to force a decay
transition on demand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
28
link/PLAN.md
28
link/PLAN.md
@@ -305,7 +305,7 @@ Counts in `hello` are a live snapshot taken on the Core thread, not a cached val
|
||||
0. ~~**Fix the build** (§3).~~ **Done.** Verified: a plain boot now logs `Core: Compiling scripts... / Build succeeded.`
|
||||
1. ~~**Transport.**~~ **Done.** `BridgeLink`: `TcpClient`, link thread + bounded drop-oldest queue, reader thread → `Timer.DelayCall`, reconnect with backoff capped at 5 s. Emits `server.hello` / `server.shutdown` / `server.crashed`, answers `ping` with `pong`. `[bridge status|reload|ping]`. Acceptance evidence in §11.
|
||||
2. ~~**Cheap event streams.**~~ **Done.** `BridgeEvents` subscribes the streams selected below. All observed on the live shard; evidence in §12.
|
||||
3. **Sweeps.** Vitals (30 s), decay-on-transition (60 s, with silent `ServerStarted` baseline), economy supply (5 min). All config-tunable; `[bridge reload` re-arms the timers.
|
||||
3. ~~**Sweeps.**~~ **Done.** `BridgeSweeps`: vitals / decay-on-transition / economy, all Core-thread timers, re-armable. Evidence in §13.
|
||||
4. **Request/response.** `char.profile`, `account.roster`, `vendor.snapshot`. Sidecar caches profiles; rate-limit requests sidecar-side.
|
||||
5. **`[link` account linking.** `CommandSystem.Register("link", AccessLevel.Player, …)`, one-time short-TTL codes in a main-thread dict, `Account.SetTag("WebsiteUserId", id)` — persists to `accounts.xml` for free. Loopback-only is the trust boundary; add a shared secret if the sidecar is ever exposed.
|
||||
6. **Town-crier inbound.** `GlobalTownCrierEntryList.Instance.AddEntry(lines, duration)` (`Scripts/Mobiles/NPCs/TownCrier.cs:96`), marshaled to the Core thread. Cap line count/length and active entries.
|
||||
@@ -348,6 +348,32 @@ Two defects were found this way and fixed:
|
||||
|
||||
---
|
||||
|
||||
## 13. Phase 3 acceptance
|
||||
|
||||
`BridgeSweeps.cs` runs three repeating Core-thread timers: vitals (`StatSweepSeconds`), house decay (`DecaySweepSeconds`), economy supply (`EconomySweepSeconds`). All re-armable via `[bridge reload`; `[bridge sweepnow` runs one of each on demand; `[bridge status` reports sweep counters.
|
||||
|
||||
Verified on the seeded world with intervals cut to 8 s:
|
||||
|
||||
- **Decay is transition-only.** Baseline recorded 29 houses **silently** on `ServerStarted`. A probe bumped one house `Somewhat → Fairly` with `SetDynamicDecay`; the next sweep emitted **exactly one** `house.decay`, none for the other 28:
|
||||
|
||||
```json
|
||||
{"kind":"house.decay","serial":"0x4004705F","from":"Somewhat","to":"Fairly",
|
||||
"map":"Trammel","x":1119,"y":1794,"z":0,"region":null,"name":"An Unnamed House",
|
||||
"ownerSerial":"0x75","ban":{"x":1112,"y":1804,"z":0},
|
||||
"builtOn":"2026-05-11T…","lastRefreshed":"2026-05-31T…"}
|
||||
```
|
||||
|
||||
- **Economy supply** emitted a snapshot each interval: `{"kind":"economy.supply","accounts":51,"gold":…}`.
|
||||
- **Vitals** correctly emitted nothing — the seeded characters are all offline (`NetState == null`). The JSON shape is the same field set proven by the Phase 1 probe; the online-emission path is not exercised without a live client.
|
||||
|
||||
Notes from the run:
|
||||
|
||||
- **`region` is null** for the seeded houses — they sit outside any named region. The handler guards `Region`, `Sign`, and `Owner` for null; all three can be absent on abandoned or oddly-placed houses.
|
||||
- The sweeps **skip emitting when the sidecar is disconnected** (`BridgeLink.Connected`), so a long outage does not fill the bounded queue with perishable snapshots. Events (Phase 2) still queue through an outage because they are not perishable; sweeps re-emit fresh state on the next tick regardless.
|
||||
- **Config duplicate keys: last write wins** (`Config.cs` does `_Entries[key] = e`), which is why the scaffolding appends test overrides to the end of `Bridge.cfg`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Phase 2 acceptance
|
||||
|
||||
The selected streams (`Login`, `Logout`, `AccountLogin`, `AccountGoldChange`, `ValidVendorPurchase`/`Sell`, `PlacePlayerVendor`, `SkillGain`, `FameChange`, `KarmaChange`, `QuestComplete`, `PlayerDeath`, `PlayerMurdered`, `OnKilledBy`, `FastWalk`, `OnPropertyChanged`, `Command`, `Before`/`AfterWorldSave`) are in `BridgeEvents.cs`. Gold, fame, karma, and the save boundaries were fired through their real code paths (`DepositGold`, the `Fame`/`Karma` setters, `World.Save()`) and observed at the stub sidecar:
|
||||
|
||||
Reference in New Issue
Block a user