// Export a creature Setup's geometry (per part, with UVs + normals + the // ClothingBase texture substitutions applied) to JSON so an offline rasterizer // can render it. Uses acdream's own tested GfxObjMesh/SetupMesh port rather than // re-deriving the dat mesh format. using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using AcDream.Core.Content; using AcDream.Core.Meshing; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Lib.IO; namespace MosswartArt; internal sealed class DatSource(DatCollection dats) : IDatObjectSource { [return: MaybeNull] public T Get(uint fileId) where T : IDBObj => dats.TryGet(fileId, out var v) ? v : default; public bool TryGet(uint fileId, [MaybeNullWhen(false)] out T value) where T : IDBObj => dats.TryGet(fileId, out value!); } internal sealed class AnimLoader(DatCollection dats) : AcDream.Core.Physics.IAnimationLoader { public Animation? LoadAnimation(uint id) => dats.TryGet(id, out var a) ? a : null; } internal static class Export { private static string F(float v) => v.ToString("R", CultureInfo.InvariantCulture); public static int Run(DatCollection dats, uint setupId, uint clothingId, string outPath, uint motionTableId = 0) { if (!dats.TryGet(setupId, out var setup) || setup is null) { Console.Error.WriteLine($"Setup 0x{setupId:X8} not found"); return 1; } // ClothingBase substitution map, at SurfaceTexture (0x05) level. var texSwap = new Dictionary(); if (clothingId != 0 && dats.TryGet(clothingId, out var ct) && ct is not null) { foreach (var baseEffect in ct.ClothingBaseEffects.Values) foreach (var objEffect in baseEffect.CloObjectEffects) foreach (var te in objEffect.CloTextureEffects) texSwap[(uint)te.OldTexture] = (uint)te.NewTexture; } var referencedTextures = new HashSet(); var src = new DatSource(dats); // Creatures do not define an upright pose in Setup.PlacementFrames -- // the idle frame has to come from the MotionTable, or every part lands // stacked on the origin. var idle = MotionResolver.GetIdleFrame(setup, src, new AnimLoader(dats), motionTableId == 0 ? null : motionTableId); Console.WriteLine(idle is null ? "idle frame: NONE (falling back to placement frame)" : $"idle frame: {idle.Frames.Count} part frames"); var refs = SetupMesh.Flatten(setup, idle); var sb = new StringBuilder(); sb.Append("{\"setup\":\"0x").Append(setupId.ToString("X8")).Append("\",\"parts\":["); for (int i = 0; i < refs.Count; i++) { var mr = refs[i]; if (!dats.TryGet(mr.GfxObjId, out var gfx) || gfx is null) continue; var subs = GfxObjMesh.Build(gfx, src); var m = mr.PartTransform; if (i > 0) sb.Append(','); sb.Append("{\"index\":").Append(i) .Append(",\"gfx\":\"0x").Append(mr.GfxObjId.ToString("X8")).Append('"') .Append(",\"m\":[") .Append(F(m.M11)).Append(',').Append(F(m.M12)).Append(',').Append(F(m.M13)).Append(',').Append(F(m.M14)).Append(',') .Append(F(m.M21)).Append(',').Append(F(m.M22)).Append(',').Append(F(m.M23)).Append(',').Append(F(m.M24)).Append(',') .Append(F(m.M31)).Append(',').Append(F(m.M32)).Append(',').Append(F(m.M33)).Append(',').Append(F(m.M34)).Append(',') .Append(F(m.M41)).Append(',').Append(F(m.M42)).Append(',').Append(F(m.M43)).Append(',').Append(F(m.M44)) .Append("],\"sub\":["); for (int s = 0; s < subs.Count; s++) { var sm = subs[s]; // Surface (0x08) -> OrigTextureId (0x05) -> ClothingBase swap. uint texId = sm.SurfaceId; if ((texId >> 24) == 0x08 && dats.TryGet(texId, out var surf) && surf is not null) texId = (uint)surf.OrigTextureId; if (texSwap.TryGetValue(texId, out var swapped)) texId = swapped; referencedTextures.Add(texId); if (s > 0) sb.Append(','); sb.Append("{\"surface\":\"0x").Append(sm.SurfaceId.ToString("X8")).Append('"') .Append(",\"tex\":\"0x").Append(texId.ToString("X8")).Append('"') .Append(",\"v\":["); for (int v = 0; v < sm.Vertices.Length; v++) { var vert = sm.Vertices[v]; if (v > 0) sb.Append(','); sb.Append('[').Append(F(vert.Position.X)).Append(',').Append(F(vert.Position.Y)).Append(',').Append(F(vert.Position.Z)) .Append(',').Append(F(vert.Normal.X)).Append(',').Append(F(vert.Normal.Y)).Append(',').Append(F(vert.Normal.Z)) .Append(',').Append(F(vert.TexCoord.X)).Append(',').Append(F(vert.TexCoord.Y)).Append(']'); } sb.Append("],\"i\":["); for (int k = 0; k < sm.Indices.Length; k++) { if (k > 0) sb.Append(','); sb.Append(sm.Indices[k]); } sb.Append("]}"); } sb.Append("]}"); } sb.Append("]}"); // The documented destination (tools/IconForge/work/) does not exist in a // fresh checkout, so create it rather than making the caller mkdir first. string outFull = Path.GetFullPath(outPath); Directory.CreateDirectory(Path.GetDirectoryName(outFull) ?? "."); File.WriteAllText(outFull, sb.ToString()); Console.WriteLine($"wrote {outPath} ({new FileInfo(outFull).Length / 1024} KB), {refs.Count} parts"); // Dump the surfaces alongside the geometry. Keeping both halves in one // command means the render pipeline has a single documented input step // and cannot be handed geometry whose textures were never extracted. string textureDir = Path.Combine(Path.GetDirectoryName(outFull) ?? ".", "textures"); Console.WriteLine($"dumping {referencedTextures.Count} referenced surfaces -> {textureDir}"); int written = Textures.Dump(dats, referencedTextures, textureDir); if (written != referencedTextures.Count) { Console.Error.WriteLine( $"WARNING: {referencedTextures.Count - written} surface(s) did not resolve; " + "the render will fall back to flat grey for those."); } return 0; } }