feat(asset-bridge): the 73 bodies action 0 could not see, and the ceiling that makes looking safe (Phase 6)

The catalogue asked every body for action 0 and reported the rest absent. 73 of
this client's bodies have no art there and real art deeper — body 820's first
drawn action is 23, and it is a horse — so they rendered as text on the bestiary.
The catalogue now falls back to the first action that has art, and the key names
that action (`body/820/a23`). 1,022 -> 1,095 rows.

Walking the action axis is the one thing that can walk off the end of a body's
slots, and the slots after a body's band are the NEXT BODY'S. Measured here: one
action past the band, 643 of 795 legacy bodies return a fully validated picture
and 452 of those are byte-identical to body+1's action 0 (body 1 action 22 is an
ettin; body 3's is an imp, both confirmed by rendering them). Phase 0's validator
cannot catch that — the record is real — so the ceiling refuses the ADDRESS, in
ResolveAnimation where every caller already goes.

The ceiling is the index banding, never `Animations.GetAnimLength`: for a body
reaching file type 5 as id 34 that function answers 22 while the arithmetic gives
13, and the difference is nine actions of another creature's art.

A fetch serves only the key the catalogue chose for that body. `body/820/a0` and
`body/400/a2` come back `unsupported` with the chosen action alongside, never by
decoding what was asked for.

`EXTRACTOR_VERSION` 2 -> 3 (unchanged input, a different answer). Protocol stays
8 — `action` on a manifest/fetch row is additive.

Deep frame keys and the bulk-fill switch that §16 planned for this phase were
NOT built: the site displays still pictures, and a complete one-direction
animation set measures 174,453 frames / 281.5 MB against no consumer (docs
§11.2, org lead 2026-09-11).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-14 01:09:38 -05:00
parent b68aac41c6
commit a9bd18e48e
3 changed files with 282 additions and 45 deletions

View File

@@ -406,11 +406,123 @@ namespace Server.Custom.Bridge
return false;
}
int actions = ActionsOf(translated, fileType);
if (action >= actions)
{
// §4.10, measured in phase 6: this is the never-sweep rule again, one axis over.
// A body's slots are contiguous and the next body's begin immediately after them,
// so `index + action * 5` past the ceiling addresses ANOTHER BODY'S action — a
// real record, at a real offset, that every check below passes. Measured on this
// client: of 795 legacy bodies, 643 return a fully validated picture one action
// past their band and **452 of those are byte-identical to body+1's action 0**.
// Body 1 action 22 is an ettin; body 3 action 22 is an imp. Nothing downstream
// can tell, which is why the refusal has to be here.
reason = "body " + body + " has " + actions + " actions in file type " + fileType
+ "; action " + action + " belongs to the next body";
return false;
}
index = AnimIndexOf(translated, fileType) + (action * 5) + direction;
return true;
}
/// <summary>
/// How many actions the index reserves for a body — the only safe ceiling, and it is
/// the banding rather than the library's own answer.
///
/// <c>Animations.GetAnimLength</c> exists and looks like the right source. It is not:
/// for a body reaching file type 5 as id 34 it answers **22** while
/// <see cref="AnimIndexOf"/> puts that body in the 65-slot band, which is **13**. The
/// two disagree on exactly one body of this client (reached by translation from body
/// 276), and taking the larger number is nine actions of somebody else's art. So the
/// count is derived from the same arithmetic that produces the offset, in the same
/// file, where the two cannot drift apart.
/// </summary>
public static bool ActionCount(int body, out int actions, out int fileType, out string reason)
{
reason = null;
actions = 0;
fileType = 0;
if (body <= 0)
{
reason = "body " + body + " is not addressable";
return false;
}
int translated = body;
int hue = 0;
try
{
Animations.Translate(ref translated, ref hue);
fileType = BodyConverter.Convert(ref translated);
}
catch (Exception e)
{
reason = "body.def/bodyconv.def lookup failed: " + e.GetType().Name;
return false;
}
if (AnimDataPath(fileType) == null)
{
reason = "bodyconv sends body " + body + " to file type " + fileType
+ ", which this client does not have";
return false;
}
actions = ActionsOf(translated, fileType);
return true;
}
/// <summary>
/// The banding of <see cref="AnimIndexOf"/>, read as an action count: a body's slots
/// are five directions per action, so the band size divided by five is how many
/// actions it owns.
/// </summary>
private static int ActionsOf(int body, int fileType)
{
return SlotsOf(body, fileType) / 5;
}
/// <summary>
/// How many index slots <see cref="AnimIndexOf"/>'s arithmetic gives this body. The
/// bands are transcribed there and their sizes here, from the same source and in the
/// same order, because a ceiling that disagrees with an offset is worse than no
/// ceiling at all.
/// </summary>
private static int SlotsOf(int body, int fileType)
{
switch (fileType)
{
case 2:
return body < 200 ? 110 : 65;
case 3:
if (body < 300)
return 65;
return body < 400 ? 110 : 175;
case 5:
// Body 34's exclusion again — it is in the second band here, so it owns 13
// actions and not 22. This is the one body `GetAnimLength` is wrong about.
if (body < 200 && body != 34)
return 110;
return body < 400 ? 65 : 175;
default: // 1 and 4 share their banding
if (body < 200)
return 110;
return body < 400 ? 65 : 175;
}
}
/// <summary>
/// <c>Animations.GetFileIndex</c>'s own arithmetic, which is private. The banding is
/// per file type and the boundaries differ between them, so this is transcribed rather