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.
}
}
}
}
}
}