using System; using System.Collections.Generic; using System.Globalization; using System.Text; using Server.Mobiles; namespace Server.Custom.Bridge { /// /// **Slug to body id — the part only the shard can do** (docs/link/v8.md §8, protocol 8, /// phase 3). /// /// The spawn atlas knows a creature by a **slug** derived from the class name it found in /// `Spawns/*.xml` ("giant-spider"). The client knows the same creature by a **body id** /// (28). Nothing in the ServUO tree declares that mapping as data. Today an operator /// bridges it by hand, grepping `Scripts/Mobiles/Normal/<Name>.cs` for `Body =`, /// which appears as a decimal, as hex (`0xD1`), as `Utility.RandomList(35, 36)` and as an /// `m_IDs[]` table — a parse that is wrong on the shard's own custom creatures, which is /// precisely the set an operator most wants pictures for. /// /// Inside ServUO the problem does not exist: construct the type, read `Body.BodyID`, /// delete it. BridgeWorld.cs already does exactly that for a different feature. /// /// **This is the one asset-plane family that does NOT run on the asset worker**, and the /// reason is the whole point of §8. Constructing and deleting a mobile is world mutation, /// so it must happen on the Core thread — while the decode in /// must happen off it, because it reads hundreds of megabytes and would stop the world for /// every player on the shard. That split is why body resolution is its own request kind /// rather than a step inside asset extraction. /// /// Two consequences follow from answering on the Core thread, and both are bounds: /// /// **The batch is small and the shard enforces the cap itself.** Every type constructed /// here runs a real constructor — packing items, rolling skills, starting AI timers — and /// all of that happens between two ticks of the world. The website chunks its own list; /// a request over names is **refused** rather /// than truncated, so the two sides cannot quietly disagree about what was answered. /// /// **It does not take the asset plane's single slot.** The slot exists to stop several /// large replies queueing at once (§3.2); this reply is a few kilobytes and the work is /// not on the worker, so claiming the slot would only make a body pass and a catalogue /// page refuse each other for no benefit. /// /// **A creature whose constructor randomises its body reports one of its variants**, not /// an error and not a set. Constructing twice to detect that would double every side /// effect above to learn something the bestiary does not render differently — both ids are /// the same creature. The answer is stable enough to cache and cheap enough to redo. /// public static class BridgeBodies { public static void Initialize() { if (!BridgeConfig.Enabled) return; BridgeBoot.RegisterHandler("assets.bodies", OnBodies); } // ── the request ────────────────────────────────────────────────────────────────────── private static void OnBodies(Dictionary o) { var reqId = BridgeJson.GetString(o, "reqId"); if (reqId == null) { // Rule 1 of the asset plane: without a correlation id this reply lands on the // event path, is persisted to the sidecar's store and broadcast to every // subscriber. Refuse rather than answer. BridgeAssets.Fail(null, "BAD_REQUEST", "assets.bodies requires a reqId"); return; } if (!BridgeConfig.AssetsEnabled) { BridgeAssets.Fail(reqId, "DISABLED", "asset extraction is disabled on this shard"); return; } var types = BridgeJson.GetStringList(o, "types"); if (types.Count == 0) { BridgeAssets.Fail(reqId, "BAD_REQUEST", "assets.bodies requires a non-empty `types` array of ServUO class names"); return; } if (types.Count > BridgeConfig.AssetBodyBatch) { // Refuse, never truncate. A silently shortened answer looks identical to a // complete one from the website's side, and the types that fell off the end would // be recorded as "asked and unanswerable" rather than "never asked". BridgeAssets.Fail(reqId, "BAD_REQUEST", "assets.bodies takes at most " + BridgeConfig.AssetBodyBatch + " types per request (asked for " + types.Count + "); send them in chunks"); return; } Reply(reqId, types); } /// /// Core thread. Constructs each type once, reads its body, deletes it. /// /// Every outcome is a **row**, never a failed request: a shard is expected to be asked /// about types it does not have (an atlas built from a tree that has since changed, a /// spawn file naming a creature from a script package the operator removed), and a /// status screen that fails the whole pass over one of those teaches an operator to /// stop pressing the button. /// private static void Reply(string reqId, List types) { var sb = BridgeJson.Begin("assets.bodies.ok"); sb.Str("reqId", reqId) .Num("extractorVersion", BridgeAssets.EXTRACTOR_VERSION) .Num("asked", types.Count); // The envelope is shared with every other family (§3.4) even though this one never // pages: the website drives the chunking, so `more` is always false and `cut` always // "end". Writing it anyway means one reader shape on the other side rather than two. var page = new BridgeAssets.PageBuilder(sb, "rows", BridgeConfig.AssetBatchBytes); int resolved = 0; foreach (var name in types) { string status; int body; Resolve(name, out body, out status); if (status == "ok") resolved++; var item = new StringBuilder(96); item.Append("{\"type\":"); BridgeJson.Text(item, name); item.Append(",\"status\":\"").Append(status).Append('"'); if (status == "ok") item.Append(",\"body\":").Append(body.ToString(CultureInfo.InvariantCulture)); item.Append('}'); // A chunk this small cannot spend the budget — the cap above is a hundred names // and the budget is half a megabyte — but the check costs nothing and the day // someone raises `AssetBodyBatch` it is the difference between a short page and a // line the sidecar drops. if (!page.TryAdd(item.ToString(), null)) break; } page.Close(); sb.Num("resolved", resolved); BridgeLink.Emit(sb.End()); } /// /// One type name to one body id. /// /// `status` is the field the website records, and the four values are four different /// things an operator can act on: /// /// ok — constructed, body read. /// unknown — no such type on this shard. The spawn file names something the /// scripts do not define, which is a real drift an operator wants to see. /// notCreature — the type exists but is not a `BaseCreature`. Spawn files /// legitimately name items and static decorations; those have no body and never will, /// so this is a permanent answer rather than a retryable failure. /// failed — the constructor threw, or the type has none that takes no /// arguments. Caught per type, because one creature whose constructor depends on a /// script package the operator removed must not cost the other ninety-nine. /// private static void Resolve(string name, out int body, out string status) { body = 0; status = "failed"; Type type; try { // `true` is ignoreCase — spawn files are hand-edited and their casing drifts from // the class it names far more often than the name itself does. type = ScriptCompiler.FindTypeByName(name, true); } catch { status = "failed"; return; } if (type == null) { status = "unknown"; return; } if (!typeof(BaseCreature).IsAssignableFrom(type) || type.IsAbstract) { status = "notCreature"; return; } BaseCreature creature = null; try { creature = Activator.CreateInstance(type) as BaseCreature; if (creature == null) { status = "failed"; return; } body = creature.Body.BodyID; status = body > 0 ? "ok" : "failed"; } catch (Exception e) { Console.WriteLine("[Bridge] assets.bodies: {0}: {1}: {2}", name, e.GetType().Name, e.Message); status = "failed"; } finally { if (creature != null) { try { // Deleting the mobile deletes the items it packed — `Mobile.Delete` walks // `Items`, and `Item.Delete` walks what each contains — and stops its AI // timer. A creature left alive here is a creature standing at (0,0,0) on // the internal map forever, saved with the world, once per import. creature.Delete(); } catch { // Nothing useful is left to do, and throwing out of `finally` would lose // whatever the try block was already reporting. } } } } } }