using System; using System.Collections.Generic; using System.Linq; using Server.Commands; using Server.Custom.Bridge; using Server.Engines.CannedEvil; using Server.Mobiles; namespace Server.Custom { /// /// Exercises the two halves of Protocol 6 on a live shard, without a game client. /// /// **Idempotency needs no probe.** It is driven from the OTHER end — two identical POSTs to /// the sidecar, the second of which must come back `replayed: true` under its own reqId — so /// a curl and the shard's own audit trail are the whole test. Nothing here would make that /// more convincing. /// /// `champ.boss.killed` is the opposite case. It cannot be produced from outside the game at /// all: a champion boss appears only when a spawn is driven to its final level, and the /// damage table the frame carries is assembled by real combat against a real creature. A /// fixture can assert the shape of the JSON; only this proves that /// `EventSink.CreatureDeath` fires for a `BaseChampion`, that `DamageEntries` still holds /// anything by the time it does, and that the sweep's spawn attribution is there to name the /// altar. /// /// What it does, in order: /// /// 1. Places a real `ChampionSpawn`, activates it and calls `SpawnChampion()` — the /// shard's own code path, not a hand-constructed creature. /// 2. Waits for the champ sweep to see it, so the boss is attributed to its altar exactly /// the way a real one would be. **This wait is the assertion**: run without it and the /// kill still emits, but with no `serial`, `type` or `level` — which is the phase's own /// documented fallback rather than the case being tested. /// 3. Damages it from two real player mobiles found in the world, so the damage table has /// two ranked entries rather than none. /// 4. Kills it and cleans up the altar. /// /// Test scaffolding. Never deployed; `deploy.ps1` copies only `overlay/`. /// In game / at the console: `[p6probe`. Flag: `Protocol6ProbeOnStart`. /// **Spawns and kills a champion boss.** Use on a rig, never on a live shard. /// public static class BridgeProtocol6Probe { private static ChampionSpawn _spawn; public static void Initialize() { CommandSystem.Register("p6probe", AccessLevel.Administrator, Probe_OnCommand); if (Config.Get("Bridge.Protocol6ProbeOnStart", false)) EventSink.ServerStarted += () => Timer.DelayCall(TimeSpan.FromSeconds(10.0), () => Run(null)); } [Usage("p6probe")] [Description("Spawns a champion boss, damages it from two players and kills it.")] private static void Probe_OnCommand(CommandEventArgs e) { Run(e == null ? null : e.Mobile); } private static void Say(Mobile to, string text) { Console.WriteLine("[p6probe] {0}", text); if (to != null) to.SendMessage(text); } private static void Run(Mobile from) { try { // Inside a NAMED region, deliberately. A champion altar really lives in a dungeon // and the first version of this probe put one there — but the dungeon floor at // Destard belongs to the map's default region, whose Name is empty, so the emitted // frame carried no `region` at all and the one field a phase condition is most // likely to match on ("the boss in Yew") went unproven. Britain has a named region, // so this exercises the field rather than the guard that omits it. var where = new Point3D(1496, 1628, 10); var map = Map.Felucca; Cleanup(); _spawn = new ChampionSpawn(); _spawn.MoveToWorld(where, map); _spawn.Type = ChampionSpawnType.Abyss; _spawn.AutoRestart = false; _spawn.Active = true; Say(from, "altar placed; spawning its champion"); _spawn.SpawnChampion(); var boss = _spawn.Champion; if (boss == null) { Say(from, "FAILED: the spawn produced no champion"); Cleanup(); return; } Say(from, String.Format("champion up: {0} ({1}) serial {2} region {3}", boss.Name, boss.GetType().Name, boss.Serial, boss.Region == null ? "(none)" : ("\"" + boss.Region.Name + "\""))); // Give the sweep time to attribute the boss to its altar. Two intervals, because a // single one races the timer that is already part-way through its period. var wait = TimeSpan.FromSeconds(Math.Max(2.0, BridgeConfig.ChampSweepSeconds * 2.0)); Say(from, String.Format("waiting {0:0}s for the champ sweep to see it", wait.TotalSeconds)); Timer.DelayCall(wait, () => Finish(from, boss)); } catch (Exception ex) { Say(from, "threw: " + ex); Cleanup(); } } private static void Finish(Mobile from, Mobile boss) { try { if (boss == null || boss.Deleted) { Say(from, "FAILED: the champion vanished before it could be killed"); Cleanup(); return; } // Two real players, so the damage table has two ranked entries and the ranking is // testable rather than trivially one row. Registered through Mobile.RegisterDamage, // which is the same call combat makes. var players = World.Mobiles.Values .OfType() .Where(p => !p.Deleted && p.Account != null) .Take(2) .ToList(); if (players.Count < 2) { Say(from, "note: fewer than two player mobiles in the world; the table will be short"); } for (int i = 0; i < players.Count; i++) { // Deliberately unequal and deliberately in ascending order, so a frame that // reported them in arrival order rather than by damage would be visibly wrong. int amount = 120 * (i + 1); boss.RegisterDamage(amount, players[i]); Say(from, String.Format("registered {0} damage from {1}", amount, players[i].Name)); } var killer = players.Count > 0 ? players[players.Count - 1] : null; Say(from, "killing the champion"); boss.Damage(boss.HitsMax * 10, killer); if (!boss.Deleted && boss.Alive) { Say(from, "note: it survived the blow; killing it outright"); boss.Kill(); } Timer.DelayCall(TimeSpan.FromSeconds(2.0), () => { Cleanup(); Say(from, "done — check the sidecar feed for champ.boss.killed"); }); } catch (Exception ex) { Say(from, "threw: " + ex); Cleanup(); } } private static void Cleanup() { if (_spawn == null) return; try { _spawn.Active = false; _spawn.Delete(); } catch { // The altar is scaffolding; failing to tidy it is not worth an exception. } _spawn = null; } } }