Phase 2: cheap event streams

BridgeEvents subscribes the streams selected for tracking, economy, and cheat
detection: Login/Logout/AccountLogin, AccountGoldChange, ValidVendorPurchase/
Sell, PlacePlayerVendor, SkillGain, FameChange, KarmaChange, QuestComplete,
PlayerDeath, PlayerMurdered, OnKilledBy, FastWalk, OnPropertyChanged, Command,
and Before/AfterWorldSave.

Every handler runs on the Core thread inside the path that raised it, so each is
wrapped to never throw, does only Emit (which enqueues and returns), and never
mutates the args. Three of these are veto hooks and are read strictly:
AccountLogin (Accepted/RejectReason, and a plaintext Password we never emit),
FastWalk (Blocked), and the login decision path generally.

Testing on the live shard found that SkillGain fires for NPCs, hard: the first
boot emitted 115 skill.gain events in four seconds, all spawned creatures
grinding Meditation, zero players. That is the general rule here — most "player"
events also fire for NPCs — so SkillGain, FameChange, KarmaChange, and OnKilledBy
all filter to players on the Core thread before the socket. Gold, fame, karma,
and the save boundaries were fired through their real code paths and observed at
the stub sidecar; gold.change round-trips the platinum->gold conversion and
persists across restarts. Evidence in docs/PLAN.md §12.

Adds tools/scaffolding/BridgeEventProbe.cs (never deployed) which triggers those
events through real world mutations rather than synthetic Invoke calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 10:46:41 -05:00
parent 9c02ba45dc
commit d5035cb6c6
5 changed files with 538 additions and 2 deletions

View File

@@ -0,0 +1,68 @@
using System;
using Server.Accounting;
using Server.Mobiles;
namespace Server.Custom
{
/// <summary>
/// Fires a handful of the bridge's event streams by doing real things to the world, so the
/// emit path and JSON shape can be verified without a game client attached.
///
/// These are genuine triggers, not synthetic EventSink.Invoke calls: DepositGold raises
/// AccountGoldChange from Account.cs:1635, the Fame/Karma setters raise theirs from
/// Mobile.cs:7121,7141, and World.Save raises the save boundaries from World.cs:1151,1202.
/// Calling Invoke directly would prove only that the handler compiles.
///
/// Test scaffolding. Never deployed. Mutates the world (gold, fame, karma) and saves.
/// Run only against a seeded throwaway world with a backup.
/// </summary>
public static class BridgeEventProbe
{
public static void Initialize()
{
if (Config.Get("Bridge.EventProbeOnStart", false))
EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(4.0), Run);
}
private static void Run()
{
try
{
var acct = Accounting.Accounts.GetAccount("seed_000") as Account;
if (acct == null)
{
Console.WriteLine("[EventProbe] no seed_000 account; seed the world first");
return;
}
var pm = acct[0] as PlayerMobile;
if (pm == null)
{
Console.WriteLine("[EventProbe] seed_000 has no character in slot 0");
return;
}
Console.WriteLine("[EventProbe] firing gold.change ...");
acct.DepositGold(12345);
Console.WriteLine("[EventProbe] firing fame.change ...");
pm.Fame = pm.Fame + 100;
Console.WriteLine("[EventProbe] firing karma.change ...");
pm.Karma = pm.Karma - 50;
Console.WriteLine("[EventProbe] firing world.save.before / world.save.after ...");
World.Save();
Console.WriteLine("[EventProbe] done");
}
catch (Exception ex)
{
Console.WriteLine("[EventProbe] FAILED: " + ex);
}
}
}
}