Skip to content

feat(frostbite): add Star Wars Squadrons / Battlefront II support#23

Open
dhkatz wants to merge 1 commit into
mainfrom
feat/frostbite
Open

feat(frostbite): add Star Wars Squadrons / Battlefront II support#23
dhkatz wants to merge 1 commit into
mainfrom
feat/frostbite

Conversation

@dhkatz
Copy link
Copy Markdown
Contributor

@dhkatz dhkatz commented May 23, 2026

Summary

  • Adds a Frostbite asset path covering static + composite mesh extraction for Star Wars: Squadrons and SWBF II. Mount a game install in the Explorer, browse MeshAsset / CompositeMeshAsset / TextureAsset entries, and preview meshes as glTF with textures resolved through MeshVariationDatabase and SurfaceShaderPreset EBX walking.
  • Self-describing EBX V2 walker (MvdbWalker, SspWalker) — no TypeLibrary / per-game SDK DLL required. BC1–7 / R8 / RGBA8 texture decode, with BC4 / BC5 routed through AssetRipper (Detex's RGTC paths AV on some Squadrons assets). Frostbite NMA packed-normal (R=Nx, G=Ny, B=Metallic, A=AO) is reconstructed into a proper RGB normal map so glTF viewers don't read Metallic as Normal.Z.
  • Per-section UV-channel heuristic (TexCoord0 vs TexCoord1) since FrostyEditor itself uses MeshFallback.hlsl and reads TexCoord0 unconditionally. The heuristic actually beats Frosty's preview for vehicles that route diffuse through TexCoord1 (e.g., the A-wing main body).

Known limitations (documented in CHANGELOG)

  • Skinned meshes throw — MeshType == Skinned is phase-2 work.
  • The window/glass material slot renders untextured (Squadrons uses a procedural glass shader, not a texture binding).
  • Meshes whose game shader applies a UV transform via constant buffer (notably *_dest_ damage-state bodies) preview with mild UV drift. The mesh data still converts correctly.

Test plan

  • Mount a Star Wars: Squadrons install via the Explorer and confirm MeshAsset / CompositeMeshAsset / TextureAsset entries appear
  • Preview Fla_Emp_Dash_CarbonizedMynock_01_mesh — diffuse + normal applied, no UV drift
  • Preview Veh_Reb_Hunt_AWing_mesh — body wears T_Veh_Reb_Hunt_AWing_01_CS, normals correct, glass is untextured (expected)
  • dotnet build GMConverter.slnx --configuration Release --no-restore passes
  • dotnet format GMConverter.slnx --verify-no-changes --severity warn --no-restore --exclude Dependencies/ passes

🤖 Generated with Claude Code

…ture extraction

Adds a Frostbite asset path covering static and composite MeshAssets,
TextureAssets, and the surrounding material binding for Squadrons (and
SWBF II, sharing the same MeshSet RES layout). The Explorer can mount
a game install and browse EBX entries; previewing a mesh produces a
glTF with diffuse / normal / emissive textures resolved through the
MeshVariationDatabase and SurfaceShaderPreset EBX entries.

Pipeline highlights:
- FrostySdk v2 vendored as a submodule under Dependencies/FrostyToolsuite,
  used for mount / asset enumeration / chunk fetch only.
- Custom MeshSet RES parser for the SWBF2 / SWS profile, including
  subset-category-aware filtering of depth-only / shadow proxies.
- BC1-7 / R8 / RGBA8 texture decode, with BC4 / BC5 routed through
  AssetRipper (Detex's RGTC paths AV on some Squadrons assets).
- Frostbite NMA packed-normal channel (R=Nx, G=Ny, B=Metallic, A=AO)
  is reconstructed into a proper RGB normal map so glTF viewers stop
  reading Metallic as Normal.Z.
- Self-describing EBX V2 walker for MeshVariationDatabase + SSP so
  material -> texture bindings work without TypeLibrary or per-game SDK.
- Per-section UV-channel heuristic (TexCoord0 vs TexCoord1) since
  FrostyEditor itself uses MeshFallback.hlsl and reads TexCoord0
  unconditionally; the heuristic gives a better preview than Frosty
  does for vehicles that route diffuse through TexCoord1.

Known limits documented in the CHANGELOG: skinned meshes are not yet
supported; the procedural-glass window slot renders untextured; and
meshes whose game shader applies a constant-buffer UV transform
(notably the *_dest_ damage-state bodies) preview with mild UV drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines +131 to +137
foreach (var hint in _cosmeticSkinHints)
{
if (path.Contains(hint, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
Comment on lines +158 to +164
foreach (var t in header.TypeDescriptors)
{
if (t.Name == name)
{
return t;
}
}
Comment on lines +114 to +120
foreach (var t in header.TypeDescriptors)
{
if (t.Name == name)
{
return t;
}
}
Comment on lines +73 to +76
catch (Exception ex)
{
PerfTimer.Log("frostbite.mount", $"Detex init failed: {ex.GetType().Name}: {ex.Message}");
}
// on first call; Initialize loads it for the static helper.
try
{
var detexPath = Path.Combine(AppContext.BaseDirectory, DetexHelper.DLL_NAME);
if (!ProfilesLibrary.Initialize(profileKey))
{
throw new GMConverterException(
$"FrostySdk ProfilesLibrary failed to load profile \"{profileKey}\". Expected {profileKey}.json under \"{Path.Combine(AppContext.BaseDirectory, "Profiles")}\".");
return false;
}

if (!File.Exists(Path.Combine(installRoot, "Data", "layout.toc")))
{
foreach (var (exeName, profileKey) in _knownGames)
{
if (File.Exists(Path.Combine(installRoot, exeName + ".exe")))
// .frostbiteref is a tiny JSON pointer the FrostbiteImporter reads back. We park each one
// in a per-resolve directory under temp so the existing UI cleanup (which deletes the
// resolved-input directory after preview/conversion) sweeps it without extra wiring.
var resolveDir = Path.Combine(Path.GetTempPath(), "GMConverter", "FrostbiteResolved", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(resolveDir);

var safeName = NameHelpers.SanitizeFileName(Path.GetFileName(fileEntry.ArchiveEntryPath));
var manifestPath = Path.Combine(resolveDir, $"{safeName}.frostbiteref");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant