using System; using System.Collections.Generic; using System.Globalization; using Server.Commands; using Server.Mobiles; namespace Server.Custom { /// /// Produces real kill credit inside a participation area, without a game client. /// /// ── What this can drive, and what it cannot ─────────────────────────────────────────── /// /// The participation ledger counts two things: presence, and kill credit. Only one of them /// is reachable from a headless rig, and the split is worth stating rather than discovering. /// /// **Presence needs a connected client.** The sweep credits online players — `NetState != /// null` — which is the correct test and not one a probe should loosen: a character parked /// in Britain and logged out for eight hours did not attend anything, and a ledger that said /// otherwise would put people at the top of a leaderboard for being AFK. There is no way to /// produce a NetState here short of writing a client, so presence accrual is exercised by a /// real login and not by this file. /// /// **Kill credit needs none.** `EventSink.CreatureDeath` fires for a creature killed by any /// means, `Mobile.DamageEntries` is populated by real damage, and the area test is a /// coordinate comparison. So the whole of the credit path — the damager filter, the /// per-damager fold, the area test applied to the DAMAGER rather than only the corpse, the /// member cap — runs exactly as it would in a fight. /// /// What it does, in order: moves two real player mobiles to the venue, spawns a creature /// there, damages it unequally from both, and kills it. /// /// Test scaffolding. Never deployed; `deploy.ps1` copies only `overlay/`. /// In game: `[partprobe `. From a headless rig, through /// `BridgeRigDriver`'s `partprobe` verb — the two ship together for that reason. /// **Moves players and spawns and kills a creature. Rig only.** /// public static class BridgeParticipationProbe { public static void Initialize() { CommandSystem.Register("partprobe", AccessLevel.Administrator, Probe_OnCommand); } [Usage("partprobe ")] [Description("Moves two players to a point, spawns a creature there and kills it.")] private static void Probe_OnCommand(CommandEventArgs e) { if (e.Length < 3) { Say(e.Mobile, "partprobe "); return; } Run(e.Mobile, e.GetString(0), e.GetInt32(1), e.GetInt32(2)); } public static void Run(Mobile from, string mapName, int x, int y) { var map = MapByName(mapName); if (map == null) { Say(from, "partprobe: unknown map " + mapName); return; } var players = FindPlayers(2); if (players.Count < 2) { Say(from, "partprobe: need two player mobiles in the world; found " + players.Count); return; } var z = map.GetAverageZ(x, y); for (int i = 0; i < players.Count; i++) { // Spread them a tile apart so neither lands inside the other, and so the area test // is answering about two distinct points rather than one. players[i].MoveToWorld(new Point3D(x + i, y, z), map); Say(from, String.Format(CultureInfo.InvariantCulture, "partprobe: {0} moved to {1} ({2}, {3})", players[i].Name, map.Name, x + i, y)); } var victim = new Mongbat(); victim.MoveToWorld(new Point3D(x, y + 1, z), map); // Real damage through the real path, unequal so the fold is doing something: the // ledger credits one kill per damager regardless of how much they did, and a table // where both did the same amount could not show that. // // **Both amounts are small on purpose, and the first run of this probe is why.** A // Mongbat has around thirty hit points, and an opening blow of 40 killed it where it // stood -- so the SECOND damager never landed a hit, `DamageEntries` held one name, // and the ledger correctly credited one player. The frame looked like a plugin bug // crediting only the killer and was a rig artefact. A probe that means to produce two // damagers has to leave the creature alive to receive the second one. var hit = Math.Max(1, victim.HitsMax / 10); victim.Damage(hit * 2, players[0]); victim.Damage(hit, players[1]); Say(from, String.Format(CultureInfo.InvariantCulture, "partprobe: {0} spawned at ({1}, {2}) and damaged by {3} and {4}", victim.Name, x, y + 1, players[0].Name, players[1].Name)); // Killed on the next tick rather than inline, so the damage above has actually been // registered against the creature before CreatureDeath reads the entries. Timer.DelayCall(TimeSpan.FromSeconds(1.0), () => { victim.Kill(); Say(from, "partprobe: killed; the credit should now be on the ledger"); }); } private static List FindPlayers(int count) { var found = new List(); foreach (var m in World.Mobiles.Values) { var pm = m as PlayerMobile; if (pm == null || pm.Deleted || pm.AccessLevel > AccessLevel.Player) continue; found.Add(pm); if (found.Count >= count) break; } return found; } private static Map MapByName(string name) { for (int i = 0; i < Map.Maps.Length; i++) { var map = Map.Maps[i]; if (map != null && String.Equals(map.Name, name, StringComparison.OrdinalIgnoreCase)) return map; } return null; } private static void Say(Mobile to, string text) { if (to != null) to.SendMessage(text); else Console.WriteLine("[PartProbe] " + text); } } }