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:
@@ -37,7 +37,7 @@ Anything under `overlay/` is authoritative. Do not edit files in the server tree
|
|||||||
| 0 — build fix (`Scripts.csproj`) | **done, verified end-to-end** |
|
| 0 — build fix (`Scripts.csproj`) | **done, verified end-to-end** |
|
||||||
| 1 — transport (`BridgeLink`) | **done, acceptance in `docs/PLAN.md` §11** |
|
| 1 — transport (`BridgeLink`) | **done, acceptance in `docs/PLAN.md` §11** |
|
||||||
| 2 — event streams (`BridgeEvents`) | **done, acceptance in `docs/PLAN.md` §12** |
|
| 2 — event streams (`BridgeEvents`) | **done, acceptance in `docs/PLAN.md` §12** |
|
||||||
| 3 — sweeps | not started |
|
| 3 — sweeps (`BridgeSweeps`) | **done, acceptance in `docs/PLAN.md` §13** |
|
||||||
| 4 — request/response | not started |
|
| 4 — request/response | not started |
|
||||||
| 5 — `[link` account linking | not started |
|
| 5 — `[link` account linking | not started |
|
||||||
| 6 — town-crier inbound | not started |
|
| 6 — town-crier inbound | not started |
|
||||||
@@ -66,6 +66,7 @@ Runtime script compilation therefore had no effect, silently. `overlay/Scripts/S
|
|||||||
| `BridgeLink.cs` | The socket. Link thread owns it; a bounded drop-oldest queue fronts it; a reader thread marshals inbound lines to the Core thread. |
|
| `BridgeLink.cs` | The socket. Link thread owns it; a bounded drop-oldest queue fronts it; a reader thread marshals inbound lines to the Core thread. |
|
||||||
| `BridgeBoot.cs` | Lifecycle, inbound dispatch, `[bridge status\|reload\|ping]`. |
|
| `BridgeBoot.cs` | Lifecycle, inbound dispatch, `[bridge status\|reload\|ping]`. |
|
||||||
| `BridgeEvents.cs` | EventSink subscriptions (Phase 2). Read-only, player-filtered, never emits secrets. |
|
| `BridgeEvents.cs` | EventSink subscriptions (Phase 2). Read-only, player-filtered, never emits secrets. |
|
||||||
|
| `BridgeSweeps.cs` | Polled streams (Phase 3): vitals, house decay on transition, economy supply. Core-thread timers. |
|
||||||
|
|
||||||
`Emit()` is called from the Core thread. It enqueues and returns — it never touches the socket, never blocks, never allocates a syscall. **A wedged or absent sidecar cannot stall the shard**, and that is the property everything else depends on.
|
`Emit()` is called from the Core thread. It enqueues and returns — it never touches the socket, never blocks, never allocates a syscall. **A wedged or absent sidecar cannot stall the shard**, and that is the property everything else depends on.
|
||||||
|
|
||||||
|
|||||||
28
docs/PLAN.md
28
docs/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.`
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
## 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:
|
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:
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ namespace Server.Custom.Bridge
|
|||||||
BridgeLink.Emit(sb.End());
|
BridgeLink.Emit(sb.End());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Usage("bridge [status | reload | ping]")]
|
[Usage("bridge [status | reload | ping | sweepnow]")]
|
||||||
[Description("Inspects and controls the sidecar link.")]
|
[Description("Inspects and controls the sidecar link.")]
|
||||||
private static void Bridge_OnCommand(CommandEventArgs e)
|
private static void Bridge_OnCommand(CommandEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -158,8 +158,9 @@ namespace Server.Custom.Bridge
|
|||||||
{
|
{
|
||||||
case "reload":
|
case "reload":
|
||||||
BridgeConfig.Load();
|
BridgeConfig.Load();
|
||||||
|
BridgeSweeps.Rearm();
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
||||||
e.Mobile.SendMessage("Bridge: endpoint changes take effect on reconnect.");
|
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ping":
|
case "ping":
|
||||||
@@ -167,12 +168,19 @@ namespace Server.Custom.Bridge
|
|||||||
e.Mobile.SendMessage("Bridge: ping queued.");
|
e.Mobile.SendMessage("Bridge: ping queued.");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "sweepnow":
|
||||||
|
BridgeSweeps.SweepOnce();
|
||||||
|
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
||||||
e.Mobile.SendMessage(
|
e.Mobile.SendMessage(
|
||||||
"Bridge: connected={0} depth={1} sent={2} dropped={3} received={4} connects={5} writeErrors={6}",
|
"Bridge: connected={0} depth={1} sent={2} dropped={3} received={4} connects={5} writeErrors={6}",
|
||||||
BridgeLink.Connected, BridgeLink.Depth, BridgeLink.Sent, BridgeLink.Dropped,
|
BridgeLink.Connected, BridgeLink.Depth, BridgeLink.Sent, BridgeLink.Dropped,
|
||||||
BridgeLink.Received, BridgeLink.Connects, BridgeLink.WriteErrors);
|
BridgeLink.Received, BridgeLink.Connects, BridgeLink.WriteErrors);
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
279
overlay/Scripts/Custom/Bridge/BridgeSweeps.cs
Normal file
279
overlay/Scripts/Custom/Bridge/BridgeSweeps.cs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Server.Accounting;
|
||||||
|
using Server.Mobiles;
|
||||||
|
using Server.Multis;
|
||||||
|
|
||||||
|
namespace Server.Custom.Bridge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The three polled streams, for state that has no EventSink: player vitals, house decay,
|
||||||
|
/// and money supply. All three run on the Core thread via repeating Timers, and the
|
||||||
|
/// measured cost (docs/PLAN.md §1) is why they can: at the seeded scale a full pass of all
|
||||||
|
/// three is well under a millisecond.
|
||||||
|
///
|
||||||
|
/// Timers do not fire during a world save (Timer.cs:322), so a sweep that would have landed
|
||||||
|
/// mid-save simply happens a few seconds later. That is fine for all three.
|
||||||
|
/// </summary>
|
||||||
|
public static class BridgeSweeps
|
||||||
|
{
|
||||||
|
private static Timer _vitals, _decay, _economy;
|
||||||
|
|
||||||
|
// Last-known decay level per house. In memory, rebuilt from a silent baseline on
|
||||||
|
// ServerStarted, so a restart does not re-announce every house's current stage.
|
||||||
|
private static readonly Dictionary<Serial, DecayLevel> _decayState =
|
||||||
|
new Dictionary<Serial, DecayLevel>();
|
||||||
|
|
||||||
|
private static bool _baselined;
|
||||||
|
|
||||||
|
private static long _vitalsSweeps, _vitalsEmitted, _decaySweeps, _decayTransitions, _economySweeps;
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
if (!BridgeConfig.Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
EventSink.ServerStarted += OnServerStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnServerStarted()
|
||||||
|
{
|
||||||
|
BaselineDecay();
|
||||||
|
Rearm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stops and recreates the timers from current config. Called by `[bridge reload`.</summary>
|
||||||
|
public static void Rearm()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
_vitals = Timer.DelayCall(
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.StatSweepSeconds),
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.StatSweepSeconds),
|
||||||
|
VitalsSweep);
|
||||||
|
|
||||||
|
_decay = Timer.DelayCall(
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.DecaySweepSeconds),
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.DecaySweepSeconds),
|
||||||
|
DecaySweep);
|
||||||
|
|
||||||
|
_economy = Timer.DelayCall(
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.EconomySweepSeconds),
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.EconomySweepSeconds),
|
||||||
|
EconomySweep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Stop()
|
||||||
|
{
|
||||||
|
if (_vitals != null) { _vitals.Stop(); _vitals = null; }
|
||||||
|
if (_decay != null) { _decay.Stop(); _decay = null; }
|
||||||
|
if (_economy != null) { _economy.Stop(); _economy = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Status()
|
||||||
|
{
|
||||||
|
return String.Format(
|
||||||
|
"vitals(sweeps={0} emitted={1}) decay(sweeps={2} transitions={3} tracked={4}) economy(sweeps={5})",
|
||||||
|
_vitalsSweeps, _vitalsEmitted, _decaySweeps, _decayTransitions, _decayState.Count, _economySweeps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- vitals ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Online players only. Vitals are small and volatile; the sidecar diffs successive
|
||||||
|
/// snapshots and forwards only changes. Offline characters do not move, so there is
|
||||||
|
/// nothing to sweep — their state is served on demand as a full profile instead.
|
||||||
|
/// </summary>
|
||||||
|
private static void VitalsSweep()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_vitalsSweeps++;
|
||||||
|
|
||||||
|
if (!BridgeLink.Connected)
|
||||||
|
return; // nothing is listening; do not fill the queue with perishable snapshots
|
||||||
|
|
||||||
|
foreach (var m in World.Mobiles.Values)
|
||||||
|
{
|
||||||
|
var pm = m as PlayerMobile;
|
||||||
|
|
||||||
|
if (pm == null || pm.NetState == null || pm.Deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
BridgeLink.Emit(WriteVitals(pm));
|
||||||
|
_vitalsEmitted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] vitals sweep threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WriteVitals(PlayerMobile m)
|
||||||
|
{
|
||||||
|
return BridgeJson.Begin("char.vitals")
|
||||||
|
.Ser("serial", m.Serial)
|
||||||
|
.Num("hits", m.Hits).Num("hitsMax", m.HitsMax)
|
||||||
|
.Num("mana", m.Mana).Num("manaMax", m.ManaMax)
|
||||||
|
.Num("stam", m.Stam).Num("stamMax", m.StamMax)
|
||||||
|
.Num("str", m.Str).Num("dex", m.Dex).Num("int", m.Int)
|
||||||
|
.Str("map", m.Map == null ? null : m.Map.Name)
|
||||||
|
.Num("x", m.X).Num("y", m.Y)
|
||||||
|
.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- house decay ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the last-known level for every house without emitting. Without this, the
|
||||||
|
/// first sweep after a restart would report every house as a fresh transition.
|
||||||
|
/// </summary>
|
||||||
|
private static void BaselineDecay()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_decayState.Clear();
|
||||||
|
|
||||||
|
foreach (var house in BaseHouse.AllHouses)
|
||||||
|
{
|
||||||
|
if (house == null || house.Deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_decayState[house.Serial] = house.DecayLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
_baselined = true;
|
||||||
|
Console.WriteLine("[Bridge] decay baseline: {0} houses", _decayState.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] decay baseline threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DecaySweep()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_decaySweeps++;
|
||||||
|
|
||||||
|
if (!_baselined)
|
||||||
|
BaselineDecay();
|
||||||
|
|
||||||
|
foreach (var house in BaseHouse.AllHouses)
|
||||||
|
{
|
||||||
|
if (house == null || house.Deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var level = house.DecayLevel; // computed getter — read once
|
||||||
|
var serial = house.Serial;
|
||||||
|
|
||||||
|
DecayLevel prior;
|
||||||
|
bool known = _decayState.TryGetValue(serial, out prior);
|
||||||
|
|
||||||
|
if (known && prior == level)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_decayState[serial] = level;
|
||||||
|
|
||||||
|
if (!known)
|
||||||
|
continue; // a house that appeared since baseline; record, do not announce
|
||||||
|
|
||||||
|
_decayTransitions++;
|
||||||
|
|
||||||
|
if (BridgeLink.Connected)
|
||||||
|
BridgeLink.Emit(WriteDecay(house, prior, level));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] decay sweep threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WriteDecay(BaseHouse house, DecayLevel from, DecayLevel to)
|
||||||
|
{
|
||||||
|
var sb = BridgeJson.Begin("house.decay")
|
||||||
|
.Ser("serial", house.Serial)
|
||||||
|
.Str("from", from.ToString())
|
||||||
|
.Str("to", to.ToString())
|
||||||
|
.Str("map", house.Map == null ? null : house.Map.Name)
|
||||||
|
.Num("x", house.X).Num("y", house.Y).Num("z", house.Z);
|
||||||
|
|
||||||
|
var region = house.Region;
|
||||||
|
if (region != null)
|
||||||
|
sb.Str("region", region.Name);
|
||||||
|
|
||||||
|
var sign = house.Sign;
|
||||||
|
if (sign != null)
|
||||||
|
sb.Str("name", sign.GetName());
|
||||||
|
|
||||||
|
var owner = house.Owner;
|
||||||
|
if (owner != null)
|
||||||
|
{
|
||||||
|
sb.Ser("ownerSerial", owner.Serial);
|
||||||
|
var acct = owner.Account as Account;
|
||||||
|
if (acct != null)
|
||||||
|
sb.Str("ownerAcct", acct.Username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where a player would physically stand to see it.
|
||||||
|
var ban = house.BanLocation;
|
||||||
|
sb.Append(",\"ban\":{\"x\":").Append(ban.X)
|
||||||
|
.Append(",\"y\":").Append(ban.Y)
|
||||||
|
.Append(",\"z\":").Append(ban.Z).Append('}');
|
||||||
|
|
||||||
|
sb.Str("builtOn", house.BuiltOn.ToUniversalTime().ToString("o"));
|
||||||
|
sb.Str("lastRefreshed", house.LastRefreshed.ToUniversalTime().ToString("o"));
|
||||||
|
|
||||||
|
return sb.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- economy supply ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Money supply = the sum of every account's currency, as a periodic snapshot. This is
|
||||||
|
/// the level; AccountGoldChange and the vendor events are the flow. The sidecar keeps
|
||||||
|
/// both.
|
||||||
|
/// </summary>
|
||||||
|
private static void EconomySweep()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_economySweeps++;
|
||||||
|
|
||||||
|
if (!BridgeLink.Connected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double totalCurrency = 0;
|
||||||
|
int accounts = 0;
|
||||||
|
|
||||||
|
foreach (Account a in Accounting.Accounts.GetAccounts())
|
||||||
|
{
|
||||||
|
totalCurrency += a.TotalCurrency;
|
||||||
|
accounts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
BridgeLink.Emit(BridgeJson.Begin("economy.supply")
|
||||||
|
.Num("accounts", accounts)
|
||||||
|
.Num("gold", (long)(totalCurrency * Account.CurrencyThreshold))
|
||||||
|
.End());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] economy sweep threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Runs each sweep once, now. For `[bridge sweepnow`.</summary>
|
||||||
|
public static void SweepOnce()
|
||||||
|
{
|
||||||
|
VitalsSweep();
|
||||||
|
DecaySweep();
|
||||||
|
EconomySweep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
tools/scaffolding/BridgeSweepProbe.cs
Normal file
70
tools/scaffolding/BridgeSweepProbe.cs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Server.Multis;
|
||||||
|
|
||||||
|
namespace Server.Custom
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Forces a house-decay transition so the decay sweep's transition detection can be
|
||||||
|
/// observed without waiting out a real 12–24 h IDOC stage.
|
||||||
|
///
|
||||||
|
/// After the bridge takes its silent baseline on ServerStarted, this bumps one condemned
|
||||||
|
/// seeded house one stage further with SetDynamicDecay. The next decay sweep should see the
|
||||||
|
/// level change and emit exactly one house.decay.
|
||||||
|
///
|
||||||
|
/// Test scaffolding. Never deployed. Only meaningful against the seeded world.
|
||||||
|
/// </summary>
|
||||||
|
public static class BridgeSweepProbe
|
||||||
|
{
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
if (Config.Get("Bridge.SweepProbeOnStart", false))
|
||||||
|
EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(6.0), Run);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Run()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BaseHouse target = null;
|
||||||
|
DecayLevel current = DecayLevel.Ageless;
|
||||||
|
|
||||||
|
// Find a house already decaying (not Ageless/LikeNew), so a bump is a real move.
|
||||||
|
foreach (var h in BaseHouse.AllHouses)
|
||||||
|
{
|
||||||
|
if (h == null || h.Deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var lvl = h.DecayLevel;
|
||||||
|
|
||||||
|
if (lvl == DecayLevel.Greatly || lvl == DecayLevel.Fairly || lvl == DecayLevel.Somewhat)
|
||||||
|
{
|
||||||
|
target = h;
|
||||||
|
current = lvl;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[SweepProbe] no decaying house found to bump");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = current + 1; // e.g. Somewhat -> Fairly -> Greatly -> IDOC
|
||||||
|
|
||||||
|
Console.WriteLine("[SweepProbe] bumping house 0x{0:X} from {1} to {2}",
|
||||||
|
target.Serial.Value, current, next);
|
||||||
|
|
||||||
|
target.SetDynamicDecay(next);
|
||||||
|
|
||||||
|
Console.WriteLine("[SweepProbe] done; the next decay sweep should emit house.decay");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[SweepProbe] FAILED: " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ These two scripts produced the measured budget in `docs/PLAN.md` §1. They are k
|
|||||||
| `BridgeSeeder.cs` | `Scripts/Custom/BridgeSeeder.cs` | Populates a synthetic world: 50 accounts, 150 characters, 30 houses, 30 player vendors with 40 listings each. |
|
| `BridgeSeeder.cs` | `Scripts/Custom/BridgeSeeder.cs` | Populates a synthetic world: 50 accounts, 150 characters, 30 houses, 30 player vendors with 40 listings each. |
|
||||||
| `BridgeProbe.cs` | `Scripts/Custom/BridgeProbe.cs` | Times every read the plugin performs, on the Core thread. Read-only. |
|
| `BridgeProbe.cs` | `Scripts/Custom/BridgeProbe.cs` | Times every read the plugin performs, on the Core thread. Read-only. |
|
||||||
| `BridgeEventProbe.cs` | `Scripts/Custom/BridgeEventProbe.cs` | Fires gold/fame/karma/save events through their real code paths so the emit path can be verified without a game client. **Mutates the world and saves.** Flag: `EventProbeOnStart`. |
|
| `BridgeEventProbe.cs` | `Scripts/Custom/BridgeEventProbe.cs` | Fires gold/fame/karma/save events through their real code paths so the emit path can be verified without a game client. **Mutates the world and saves.** Flag: `EventProbeOnStart`. |
|
||||||
|
| `BridgeSweepProbe.cs` | `Scripts/Custom/BridgeSweepProbe.cs` | Bumps one seeded house's decay stage after baseline so the decay sweep's transition detection can be observed without waiting a real IDOC stage. Flag: `SweepProbeOnStart`. Pair with short `*SweepSeconds` overrides. |
|
||||||
|
|
||||||
## Deploy overwrites Bridge.cfg
|
## Deploy overwrites Bridge.cfg
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user