feat(net+app): TextureChanges applied via Surface→OrigTex resolution (Phase 5a)
Finishes the TextureChange half of ObjDesc. Characters' clothing now
renders with correct per-part textures (user-verified "looks good"
after previous "partial coverage" / "wrong clothes"). The Nullified
Statue still looks like a flesh-colored drudge because the statue's
color comes from SubPalettes (palette-indexed texture recoloring),
which is the remaining major Phase 5 piece.
The first attempt at TextureChange application was silently broken by
an ID-type mismatch: the server encodes OldTexture/NewTexture as
SurfaceTexture (0x05XXXXXX) ids, but my sub-meshes are keyed by
Surface (0x08XXXXXX) ids. The override dict was keyed by one type
and looked up by the other, so TryGetValue never hit and no override
actually applied.
Diagnosed via Phase 1 systematic debugging with resolve-level logging:
live: spawn +Acdream texChanges=20
live: texChange part=0 old=0x05000BB0 new=0x0500025D
...
live: resolve part=0 surface=0x08000519 origTex=0x05000BB0 [MATCH]
live: resolve part=0 surface=0x0800051C origTex=0x05000CBE [MATCH]
... 10/10 lines [MATCH]
The [MATCH] lines proved the server's OldTexture IS reachable via a
Surface→OrigTextureId lookup, just needed keying by the right value.
Fix:
- TextureCache.GetOrUploadWithOrigTextureOverride(surfaceId, origTexOverride):
loads the base Surface dat for its color/flags/palette, but
substitutes the override SurfaceTexture id in the decode chain.
Caches under a (surfaceId, origTexOverride) composite key.
- MeshRef.SurfaceOverrides is now Dictionary<uint, uint> keyed by
Surface id, value = replacement OrigTextureId. Null means no
overrides.
- GameWindow.OnLiveEntitySpawned now does TWO passes when texture
changes are present:
1. Group the raw server changes by PartIndex into (oldOrigTex →
newOrigTex) dicts
2. For each affected part's post-animPartChange GfxObj, iterate
its Surfaces list, resolve each Surface → OrigTextureId, and
if that matches a raw change's oldOrigTex, write an entry
Surface id → newOrigTex into the final override map
- StaticMeshRenderer.Draw: when sub-mesh surface id has an override,
call GetOrUploadWithOrigTextureOverride instead of GetOrUpload.
Verified live: +Acdream's clothing renders correctly, NPCs are
"much better" (characters previously naked are now dressed). Statue
has the full mechanical pipeline working (resolve diagnostic shows
2/2 Surfaces [MATCH] for the statue's override dict) but its visible
color comes from the separate SubPalette overlay that isn't wired yet.
Also added a statue-targeted diagnostic block that dumps its full
ObjDesc contents (texChanges + subPalettes + animPartChanges) by
name match, which is how I traced the Nullified Statue of a Drudge's
specific ObjDesc. Lives under `if (isStatue && ...)` so normal logins
aren't spammed.
Cross-referenced against two new references this session:
* references/Chorizite.ACProtocol (cloned from github.com/Chorizite/
Chorizite.ACProtocol.git on user's suggestion) — confirms the
ObjDesc field order and PackedDword-of-known-type convention.
* references/WorldBuilder/... (already in repo) — confirms the
Surface→OrigTexture→SurfaceTexture→RenderSurface chain and the
P8/INDEX16 palette decode path.
Tests: 77 core + 83 net = 160, all green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ab24c9982
commit
b69d776179
6 changed files with 255 additions and 22 deletions
|
|
@ -529,8 +529,35 @@ public sealed class GameWindow : IDisposable
|
|||
string setupStr = spawn.SetupTableId is { } su ? $"0x{su:X8}" : "no-setup";
|
||||
string nameStr = spawn.Name is { Length: > 0 } n ? $"\"{n}\"" : "no-name";
|
||||
int animPartCount = spawn.AnimPartChanges?.Count ?? 0;
|
||||
int texChangeCount = spawn.TextureChanges?.Count ?? 0;
|
||||
int subPalCount = spawn.SubPalettes?.Count ?? 0;
|
||||
Console.WriteLine(
|
||||
$"live: spawn guid=0x{spawn.Guid:X8} name={nameStr} setup={setupStr} pos={posStr} animParts={animPartCount}");
|
||||
$"live: spawn guid=0x{spawn.Guid:X8} name={nameStr} setup={setupStr} pos={posStr} " +
|
||||
$"animParts={animPartCount} texChanges={texChangeCount} subPalettes={subPalCount}");
|
||||
|
||||
// Target the statue specifically for full diagnostic dump: Name match
|
||||
// is cheap and gives us exactly one entity's worth of log regardless
|
||||
// of arrival order.
|
||||
bool isStatue = spawn.Name is not null && spawn.Name.Contains("Statue", StringComparison.OrdinalIgnoreCase);
|
||||
if (isStatue && (texChangeCount > 0 || subPalCount > 0 || animPartCount > 0))
|
||||
{
|
||||
if (spawn.TextureChanges is { } tcs)
|
||||
{
|
||||
foreach (var tc in tcs)
|
||||
Console.WriteLine($"live: [STATUE] texChange part={tc.PartIndex} old=0x{tc.OldTexture:X8} new=0x{tc.NewTexture:X8}");
|
||||
}
|
||||
if (spawn.SubPalettes is { } sps)
|
||||
{
|
||||
Console.WriteLine($"live: [STATUE] basePalette=0x{(spawn.BasePaletteId ?? 0):X8}");
|
||||
foreach (var subPal in sps)
|
||||
Console.WriteLine($"live: [STATUE] subPalette id=0x{subPal.SubPaletteId:X8} offset={subPal.Offset} length={subPal.Length}");
|
||||
}
|
||||
if (spawn.AnimPartChanges is { } apcs)
|
||||
{
|
||||
foreach (var apc in apcs)
|
||||
Console.WriteLine($"live: [STATUE] animPart index={apc.PartIndex} newModel=0x{apc.NewModelId:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
if (_dats is null || _staticMesh is null) return;
|
||||
if (spawn.Position is null || spawn.SetupTableId is null)
|
||||
|
|
@ -589,14 +616,91 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
foreach (var mr in parts)
|
||||
// Build per-part texture overrides. The server sends TextureChanges as
|
||||
// (partIdx, oldSurfaceTextureId, newSurfaceTextureId) where both ids
|
||||
// are in the SurfaceTexture (0x05) range. Our sub-meshes are keyed
|
||||
// by Surface (0x08) ids whose `OrigTextureId` field points to a
|
||||
// SurfaceTexture. So we have to resolve each Surface → OrigTextureId,
|
||||
// match that against the part's oldSurfaceTextureId set, and build
|
||||
// a new dict keyed by Surface id → replacement OrigTextureId. The
|
||||
// renderer then calls TextureCache.GetOrUploadWithOrigTextureOverride
|
||||
// to get a texture decoded with the replacement SurfaceTexture
|
||||
// substituted inside the Surface's decode chain.
|
||||
var textureChanges = spawn.TextureChanges ?? Array.Empty<AcDream.Core.Net.Messages.CreateObject.TextureChange>();
|
||||
Dictionary<int, Dictionary<uint, uint>>? resolvedOverridesByPart = null;
|
||||
if (textureChanges.Count > 0)
|
||||
{
|
||||
// First pass: group (oldOrigTex → newOrigTex) per part.
|
||||
var perPartOldToNew = new Dictionary<int, Dictionary<uint, uint>>();
|
||||
foreach (var tc in textureChanges)
|
||||
{
|
||||
if (!perPartOldToNew.TryGetValue(tc.PartIndex, out var dict))
|
||||
{
|
||||
dict = new Dictionary<uint, uint>();
|
||||
perPartOldToNew[tc.PartIndex] = dict;
|
||||
}
|
||||
// Last write wins — matches observed duplicate semantics.
|
||||
dict[tc.OldTexture] = tc.NewTexture;
|
||||
}
|
||||
|
||||
// Second pass: resolve each affected part's Surface chain and
|
||||
// build the Surface-id-keyed override map the renderer consumes.
|
||||
bool isStatueDiag = spawn.Name is not null && spawn.Name.Contains("Statue", StringComparison.OrdinalIgnoreCase);
|
||||
resolvedOverridesByPart = new Dictionary<int, Dictionary<uint, uint>>();
|
||||
for (int pi = 0; pi < parts.Count; pi++)
|
||||
{
|
||||
if (!perPartOldToNew.TryGetValue(pi, out var oldToNew)) continue;
|
||||
var partGfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(parts[pi].GfxObjId);
|
||||
if (partGfx is null)
|
||||
{
|
||||
if (isStatueDiag)
|
||||
Console.WriteLine($"live: [STATUE] resolve part={pi} GfxObj 0x{parts[pi].GfxObjId:X8} missing");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isStatueDiag)
|
||||
Console.WriteLine($"live: [STATUE] resolve part={pi} gfx=0x{parts[pi].GfxObjId:X8} surfaces={partGfx.Surfaces.Count}");
|
||||
|
||||
Dictionary<uint, uint>? resolved = null;
|
||||
foreach (var surfQid in partGfx.Surfaces)
|
||||
{
|
||||
uint surfId = (uint)surfQid;
|
||||
var surfDat = _dats.Get<DatReaderWriter.DBObjs.Surface>(surfId);
|
||||
if (surfDat is null) continue;
|
||||
uint origTexId = (uint)surfDat.OrigTextureId;
|
||||
bool hit = origTexId != 0 && oldToNew.TryGetValue(origTexId, out uint newOrigTex) && (newOrigTex != 0 || true);
|
||||
if (isStatueDiag)
|
||||
Console.WriteLine($"live: [STATUE] surface=0x{surfId:X8} origTex=0x{origTexId:X8} " + (hit ? "[MATCH]" : "[miss]"));
|
||||
if (origTexId == 0) continue;
|
||||
if (oldToNew.TryGetValue(origTexId, out uint newId))
|
||||
{
|
||||
resolved ??= new Dictionary<uint, uint>();
|
||||
resolved[surfId] = newId;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolved is not null)
|
||||
resolvedOverridesByPart[pi] = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
for (int partIdx = 0; partIdx < parts.Count; partIdx++)
|
||||
{
|
||||
var mr = parts[partIdx];
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform));
|
||||
|
||||
IReadOnlyDictionary<uint, uint>? surfaceOverrides = null;
|
||||
if (resolvedOverridesByPart is not null && resolvedOverridesByPart.TryGetValue(partIdx, out var partOverrides))
|
||||
surfaceOverrides = partOverrides;
|
||||
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform)
|
||||
{
|
||||
SurfaceOverrides = surfaceOverrides,
|
||||
});
|
||||
}
|
||||
if (meshRefs.Count == 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -99,7 +99,21 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
uint tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
// Honor per-part surface overrides from CreateObject's
|
||||
// TextureChanges. The map is Surface id (0x08) → replacement
|
||||
// OrigTextureId (0x05 SurfaceTexture). A hit means "decode
|
||||
// this Surface but swap its OrigTextureId for the override."
|
||||
uint tex;
|
||||
if (meshRef.SurfaceOverrides is not null
|
||||
&& meshRef.SurfaceOverrides.TryGetValue(sub.SurfaceId, out var overrideOrigTex))
|
||||
{
|
||||
tex = _textures.GetOrUploadWithOrigTextureOverride(sub.SurfaceId, overrideOrigTex);
|
||||
}
|
||||
else
|
||||
{
|
||||
tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
}
|
||||
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ public sealed unsafe class TextureCache : IDisposable
|
|||
private readonly GL _gl;
|
||||
private readonly DatCollection _dats;
|
||||
private readonly Dictionary<uint, uint> _handlesBySurfaceId = new();
|
||||
/// <summary>
|
||||
/// Composite cache for surface-with-override-origtex entries (Phase 5
|
||||
/// TextureChanges). Key = (baseSurfaceId, overrideOrigTextureId),
|
||||
/// value = GL texture handle.
|
||||
/// </summary>
|
||||
private readonly Dictionary<(uint surfaceId, uint origTexOverride), uint> _handlesByOverridden = new();
|
||||
private uint _magentaHandle;
|
||||
|
||||
public TextureCache(GL gl, DatCollection dats)
|
||||
|
|
@ -30,27 +36,54 @@ public sealed unsafe class TextureCache : IDisposable
|
|||
if (_handlesBySurfaceId.TryGetValue(surfaceId, out var h))
|
||||
return h;
|
||||
|
||||
var decoded = DecodeFromDats(surfaceId);
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: null);
|
||||
h = UploadRgba8(decoded);
|
||||
_handlesBySurfaceId[surfaceId] = h;
|
||||
return h;
|
||||
}
|
||||
|
||||
private DecodedTexture DecodeFromDats(uint surfaceId)
|
||||
/// <summary>
|
||||
/// Get or upload a texture for a Surface id but with its
|
||||
/// <c>OrigTextureId</c> replaced by <paramref name="overrideOrigTextureId"/>.
|
||||
/// The Surface's other properties (type flags, color, translucency,
|
||||
/// clipmap handling, default palette) are preserved — only the
|
||||
/// SurfaceTexture lookup is swapped. This is how the server's
|
||||
/// CreateObject.TextureChanges are applied at render time.
|
||||
/// Caches under a composite key so multiple entities can share.
|
||||
/// </summary>
|
||||
public uint GetOrUploadWithOrigTextureOverride(uint surfaceId, uint overrideOrigTextureId)
|
||||
{
|
||||
var key = (surfaceId, overrideOrigTextureId);
|
||||
if (_handlesByOverridden.TryGetValue(key, out var h))
|
||||
return h;
|
||||
|
||||
var decoded = DecodeFromDats(surfaceId, origTextureOverride: overrideOrigTextureId);
|
||||
h = UploadRgba8(decoded);
|
||||
_handlesByOverridden[key] = h;
|
||||
return h;
|
||||
}
|
||||
|
||||
private DecodedTexture DecodeFromDats(uint surfaceId, uint? origTextureOverride)
|
||||
{
|
||||
var surface = _dats.Get<Surface>(surfaceId);
|
||||
if (surface is null)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
// Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue
|
||||
// instead of a texture chain. Without this, surfaces with no texture
|
||||
// would fall through to the magenta fallback. Translucency is honored
|
||||
// so Base1Solid|Translucent surfaces with Translucency=1.0 become
|
||||
// alpha=0, which the mesh shader's discard cutout makes invisible.
|
||||
// instead of a texture chain. Overrides are irrelevant here — there's
|
||||
// no texture chain to swap — so the override is ignored for solid-color
|
||||
// surfaces. Translucency is honored so Base1Solid|Translucent surfaces
|
||||
// with Translucency=1.0 become alpha=0, which the mesh shader's discard
|
||||
// cutout makes invisible.
|
||||
if (surface.Type.HasFlag(SurfaceType.Base1Solid) || (uint)surface.OrigTextureId == 0)
|
||||
return SurfaceDecoder.DecodeSolidColor(surface.ColorValue, surface.Translucency);
|
||||
|
||||
var surfaceTexture = _dats.Get<SurfaceTexture>((uint)surface.OrigTextureId);
|
||||
// Use the override SurfaceTexture id when present, otherwise the
|
||||
// Surface's native OrigTextureId. This is the whole point of the
|
||||
// override path — caller says "this Surface, but with a different
|
||||
// SurfaceTexture underneath."
|
||||
uint surfaceTextureId = origTextureOverride ?? (uint)surface.OrigTextureId;
|
||||
var surfaceTexture = _dats.Get<SurfaceTexture>(surfaceTextureId);
|
||||
if (surfaceTexture is null || surfaceTexture.Textures.Count == 0)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
|
|
@ -99,6 +132,9 @@ public sealed unsafe class TextureCache : IDisposable
|
|||
foreach (var h in _handlesBySurfaceId.Values)
|
||||
_gl.DeleteTexture(h);
|
||||
_handlesBySurfaceId.Clear();
|
||||
foreach (var h in _handlesByOverridden.Values)
|
||||
_gl.DeleteTexture(h);
|
||||
_handlesByOverridden.Clear();
|
||||
if (_magentaHandle != 0)
|
||||
{
|
||||
_gl.DeleteTexture(_magentaHandle);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ public static class CreateObject
|
|||
|
||||
/// <summary>AC dat id type prefix for GfxObj (visual model) ids.</summary>
|
||||
public const uint GfxObjTypePrefix = 0x01000000u;
|
||||
/// <summary>Palette dat id type prefix.</summary>
|
||||
public const uint PaletteTypePrefix = 0x04000000u;
|
||||
/// <summary>SurfaceTexture dat id type prefix.</summary>
|
||||
public const uint SurfaceTextureTypePrefix = 0x05000000u;
|
||||
|
||||
[Flags]
|
||||
public enum PhysicsDescriptionFlag : uint
|
||||
|
|
@ -82,8 +86,30 @@ public static class CreateObject
|
|||
ServerPosition? Position,
|
||||
uint? SetupTableId,
|
||||
IReadOnlyList<AnimPartChange> AnimPartChanges,
|
||||
IReadOnlyList<TextureChange> TextureChanges,
|
||||
IReadOnlyList<SubPaletteSwap> SubPalettes,
|
||||
uint? BasePaletteId,
|
||||
string? Name);
|
||||
|
||||
/// <summary>
|
||||
/// Server instruction to replace the surface texture at
|
||||
/// <paramref name="PartIndex"/> that currently uses
|
||||
/// <paramref name="OldTexture"/> with <paramref name="NewTexture"/>.
|
||||
/// Used to paint armor pieces the right color, make the statue
|
||||
/// look stone instead of flesh, etc.
|
||||
/// </summary>
|
||||
public readonly record struct TextureChange(byte PartIndex, uint OldTexture, uint NewTexture);
|
||||
|
||||
/// <summary>
|
||||
/// Palette-range swap: overlay <paramref name="SubPaletteId"/>'s colors
|
||||
/// into the entity's base palette starting at index <paramref name="Offset"/>
|
||||
/// for <paramref name="Length"/> colors. Used for skin/hair color
|
||||
/// on characters and team-color variations. Both Offset and Length
|
||||
/// are encoded as 8-bit values that the client historically multiplies
|
||||
/// by 8 to get the final palette index.
|
||||
/// </summary>
|
||||
public readonly record struct SubPaletteSwap(uint SubPaletteId, byte Offset, byte Length);
|
||||
|
||||
/// <summary>A server-side position: landblock id + local XYZ + unit quaternion rotation.</summary>
|
||||
public readonly record struct ServerPosition(
|
||||
uint LandblockId,
|
||||
|
|
@ -126,22 +152,32 @@ public static class CreateObject
|
|||
byte textureChangeCount = body[pos]; pos += 1;
|
||||
byte animPartChangeCount = body[pos]; pos += 1;
|
||||
|
||||
uint? basePaletteId = null;
|
||||
if (subPaletteCount > 0)
|
||||
_ = ReadPackedDword(body, ref pos); // overall palette id
|
||||
basePaletteId = ReadPackedDwordOfKnownType(body, ref pos, PaletteTypePrefix);
|
||||
|
||||
var subPalettes = subPaletteCount == 0
|
||||
? (IReadOnlyList<SubPaletteSwap>)Array.Empty<SubPaletteSwap>()
|
||||
: new SubPaletteSwap[subPaletteCount];
|
||||
for (int i = 0; i < subPaletteCount; i++)
|
||||
{
|
||||
_ = ReadPackedDword(body, ref pos); // subPaletteId
|
||||
uint subPalId = ReadPackedDwordOfKnownType(body, ref pos, PaletteTypePrefix);
|
||||
if (body.Length - pos < 2) return null;
|
||||
pos += 2; // offset + length bytes
|
||||
byte offset = body[pos]; pos += 1;
|
||||
byte length = body[pos]; pos += 1;
|
||||
((SubPaletteSwap[])subPalettes)[i] = new SubPaletteSwap(subPalId, offset, length);
|
||||
}
|
||||
|
||||
var textureChanges = textureChangeCount == 0
|
||||
? (IReadOnlyList<TextureChange>)Array.Empty<TextureChange>()
|
||||
: new TextureChange[textureChangeCount];
|
||||
for (int i = 0; i < textureChangeCount; i++)
|
||||
{
|
||||
if (body.Length - pos < 1) return null;
|
||||
pos += 1; // partIndex
|
||||
_ = ReadPackedDword(body, ref pos); // oldTexture
|
||||
_ = ReadPackedDword(body, ref pos); // newTexture
|
||||
byte partIndex = body[pos]; pos += 1;
|
||||
uint oldTex = ReadPackedDwordOfKnownType(body, ref pos, SurfaceTextureTypePrefix);
|
||||
uint newTex = ReadPackedDwordOfKnownType(body, ref pos, SurfaceTextureTypePrefix);
|
||||
((TextureChange[])textureChanges)[i] = new TextureChange(partIndex, oldTex, newTex);
|
||||
}
|
||||
|
||||
// Extract AnimPartChanges — the server uses these to replace
|
||||
|
|
@ -279,11 +315,14 @@ public static class CreateObject
|
|||
catch { /* truncated name — partial result is still useful */ }
|
||||
}
|
||||
|
||||
return new Parsed(guid, position, setupTableId, animParts, name);
|
||||
return new Parsed(guid, position, setupTableId, animParts,
|
||||
textureChanges, subPalettes, basePaletteId, name);
|
||||
|
||||
// Local helper: if we ran out of fields past PhysicsData, still
|
||||
// return the useful prefix (guid/position/setup/animParts).
|
||||
Parsed PartialResult() => new(guid, position, setupTableId, animParts, null);
|
||||
// return the useful prefix (guid/position/setup/animParts/textures/palettes).
|
||||
Parsed PartialResult() => new(
|
||||
guid, position, setupTableId, animParts,
|
||||
textureChanges, subPalettes, basePaletteId, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ public sealed class WorldSession : IDisposable
|
|||
CreateObject.ServerPosition? Position,
|
||||
uint? SetupTableId,
|
||||
IReadOnlyList<CreateObject.AnimPartChange> AnimPartChanges,
|
||||
IReadOnlyList<CreateObject.TextureChange> TextureChanges,
|
||||
IReadOnlyList<CreateObject.SubPaletteSwap> SubPalettes,
|
||||
uint? BasePaletteId,
|
||||
string? Name);
|
||||
|
||||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||
|
|
@ -228,6 +231,9 @@ public sealed class WorldSession : IDisposable
|
|||
parsed.Value.Position,
|
||||
parsed.Value.SetupTableId,
|
||||
parsed.Value.AnimPartChanges,
|
||||
parsed.Value.TextureChanges,
|
||||
parsed.Value.SubPalettes,
|
||||
parsed.Value.BasePaletteId,
|
||||
parsed.Value.Name));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,38 @@ using System.Numerics;
|
|||
|
||||
namespace AcDream.Core.World;
|
||||
|
||||
public readonly record struct MeshRef(uint GfxObjId, Matrix4x4 PartTransform);
|
||||
/// <summary>
|
||||
/// One part of a mesh: a GfxObj id + its local transform in the owning
|
||||
/// entity's frame. Optionally carries a set of server-specified surface
|
||||
/// overrides (<see cref="SurfaceOverrides"/>) so the renderer can
|
||||
/// substitute textures at draw time without having to change the base
|
||||
/// <c>Setup</c> / <c>GfxObj</c> dat lookup — e.g. the Nullified Statue of
|
||||
/// a Drudge remaps the drudge mesh's flesh textures to stone textures
|
||||
/// and characters remap their armor piece textures to match the clothing
|
||||
/// item they're wearing.
|
||||
/// </summary>
|
||||
public readonly record struct MeshRef(uint GfxObjId, Matrix4x4 PartTransform)
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// <b>Surface id (0x08XXXXXX)</b> → <b>replacement OrigTextureId
|
||||
/// (0x05XXXXXX SurfaceTexture)</b>. When the renderer is about to
|
||||
/// bind a sub-mesh's texture, it consults this map. A hit means
|
||||
/// "use the base Surface's colors/flags/palette but swap its
|
||||
/// <c>OrigTextureId</c> for the value here." The renderer calls
|
||||
/// <c>TextureCache.GetOrUploadWithOrigTextureOverride</c> which caches
|
||||
/// the decoded result under a composite key so multiple entities can
|
||||
/// share the same override without redecoding.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Null means "no overrides, use each sub-mesh's native surface as-is."
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Why not key by SurfaceTexture id directly?</b> Because sub-meshes
|
||||
/// are keyed by Surface id (0x08) in the GfxObj; we have to resolve
|
||||
/// each one to its SurfaceTexture id at hydration time so the render
|
||||
/// hot path only does a single dict lookup.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<uint, uint>? SurfaceOverrides { get; init; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue