Skip to content

Commit 8957d3e

Browse files
Add ModelToNifResolver and related tests
Introduced ModelToNifResolver to resolve model names to NIF asset names using flat and asset metadata. Added comprehensive unit tests for resolver logic. Updated AssetIndex initialization and Unity project file to include necessary source files.
1 parent ce94587 commit 8957d3e

4 files changed

Lines changed: 255 additions & 4 deletions

File tree

Maple2.File.Parser/Flat/Convert/AssetIndex.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class AssetIndex {
1010

1111
private readonly Dictionary<string, List<string>> llidLookup;
1212
private readonly Dictionary<string, Dictionary<string, string>> ntLookup;
13-
private static readonly string[] NtTagFiles = [
13+
private static readonly string[] NtTagFiles = new string[] {
1414
"application",
1515
"cn",
1616
"dds",
@@ -31,12 +31,12 @@ public class AssetIndex {
3131
"shader",
3232
"x-shockwave-flash",
3333
"x-world",
34-
];
35-
private static readonly string[] NtFiles = [
34+
};
35+
private static readonly string[] NtFiles = new string[] {
3636
"llid",
3737
"name",
3838
"relpath",
39-
];
39+
};
4040

4141
public AssetIndex(M2dReader reader) {
4242
ntLookup = new Dictionary<string, Dictionary<string, string>>();
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using Maple2.File.IO;
2+
3+
namespace Maple2.File.Parser.Flat;
4+
5+
/// <summary>
6+
/// Resolves model names to their corresponding NIF asset names.
7+
/// Sequence: modelName -> find flat with the same name -> get llid from it -> llid to uuid -> uuid to nif asset name
8+
/// </summary>
9+
public class ModelToNifResolver {
10+
private readonly FlatTypeIndex flatIndex;
11+
private readonly Convert.AssetIndex assetIndex;
12+
13+
/// <summary>
14+
/// Initializes a new instance of the ModelToNifResolver class.
15+
/// </summary>
16+
/// <param name="exportedReader">M2dReader for the exported .m2d containing flat files</param>
17+
/// <param name="assetMetadataReader">M2dReader for the asset-web-metadata.m2d file</param>
18+
/// <param name="flatRoot">Root path for flat files (default: "flat")</param>
19+
public ModelToNifResolver(M2dReader exportedReader, M2dReader assetMetadataReader, string flatRoot = "flat") {
20+
flatIndex = new FlatTypeIndex(exportedReader, flatRoot);
21+
assetIndex = new Convert.AssetIndex(assetMetadataReader);
22+
}
23+
24+
/// <summary>
25+
/// Resolves a model name to its NIF asset name.
26+
/// </summary>
27+
/// <param name="modelName">The name of the model to resolve</param>
28+
/// <returns>The NIF asset name, or null if not found or not a NIF asset</returns>
29+
public string? GetNifAssetName(string modelName) {
30+
// Step 1: Find flat type with the same name
31+
FlatType? flatType = flatIndex.GetType(modelName);
32+
if (flatType == null) {
33+
Console.WriteLine($"Model not found: {modelName}");
34+
return null;
35+
}
36+
37+
// Step 2: Get llid from the flat type
38+
// NIF assets are typically stored in properties like "NifAsset" or "AttachedNifAsset"
39+
string? llid = GetNifAssetLlid(flatType);
40+
if (llid == null) {
41+
Console.WriteLine($"No NIF asset found for model: {modelName}");
42+
return null;
43+
}
44+
45+
// Step 3 & 4: Convert llid to uuid and get asset name
46+
(string name, string path, string tags) = assetIndex.GetFields(llid);
47+
48+
if (string.IsNullOrEmpty(name)) {
49+
Console.WriteLine($"Failed to resolve asset metadata for llid: {llid}");
50+
return null;
51+
}
52+
53+
// Verify it's actually a NIF asset by checking tags or path
54+
if (!IsNifAsset(path, tags)) {
55+
Console.WriteLine($"Asset is not a NIF file: {name} (path: {path}, tags: {tags})");
56+
return null;
57+
}
58+
59+
return name;
60+
}
61+
62+
/// <summary>
63+
/// Gets detailed information about the NIF asset for a model.
64+
/// </summary>
65+
/// <param name="modelName">The name of the model to resolve</param>
66+
/// <returns>A tuple containing (name, path, tags) of the NIF asset, or null values if not found</returns>
67+
public (string? Name, string? Path, string? Tags) GetNifAssetInfo(string modelName) {
68+
// Step 1: Find flat type with the same name
69+
FlatType? flatType = flatIndex.GetType(modelName);
70+
if (flatType == null) {
71+
Console.WriteLine($"Model not found: {modelName}");
72+
return (null, null, null);
73+
}
74+
75+
// Step 2: Get llid from the flat type
76+
string? llid = GetNifAssetLlid(flatType);
77+
if (llid == null) {
78+
Console.WriteLine($"No NIF asset found for model: {modelName}");
79+
return (null, null, null);
80+
}
81+
82+
// Step 3 & 4: Convert llid to uuid and get asset info
83+
(string name, string path, string tags) = assetIndex.GetFields(llid);
84+
85+
if (string.IsNullOrEmpty(name)) {
86+
Console.WriteLine($"Failed to resolve asset metadata for llid: {llid}");
87+
return (null, null, null);
88+
}
89+
90+
return (name, path, tags);
91+
}
92+
93+
/// <summary>
94+
/// Extracts the NIF asset LLID from a flat type.
95+
/// Searches for properties like "NifAsset", "AttachedNifAsset", or similar.
96+
/// </summary>
97+
private string? GetNifAssetLlid(FlatType flatType) {
98+
// Common property names for NIF assets
99+
string[] nifPropertyNames = { "NifAsset", "AttachedNifAsset", "Mesh", "MeshAsset" };
100+
101+
foreach (string propertyName in nifPropertyNames) {
102+
FlatProperty? property = flatType.GetProperty(propertyName);
103+
if (property == null) {
104+
continue;
105+
}
106+
107+
// Check if the property type is AssetID or related
108+
if (property.Type == "AssetID" || property.Type.Contains("Asset")) {
109+
string? llid = property.Value?.ToString();
110+
if (!string.IsNullOrEmpty(llid)) {
111+
return llid;
112+
}
113+
}
114+
115+
// For AssocAttachedNifAsset, get the first value
116+
if (property.Type == "AssocAttachedNifAsset" && property.Value is Dictionary<string, string> dict) {
117+
string? firstValue = dict.Values.FirstOrDefault();
118+
if (!string.IsNullOrEmpty(firstValue)) {
119+
return firstValue;
120+
}
121+
}
122+
}
123+
124+
// If no NIF property found, try to get all properties with AssetID type
125+
foreach (FlatProperty property in flatType.GetAllProperties()) {
126+
if (property.Type == "AssetID") {
127+
string? llid = property.Value?.ToString();
128+
if (!string.IsNullOrEmpty(llid)) {
129+
// Check if this is likely a NIF asset
130+
(string name, string path, string tags) = assetIndex.GetFields(llid);
131+
if (IsNifAsset(path, tags)) {
132+
return llid;
133+
}
134+
}
135+
}
136+
}
137+
138+
return null;
139+
}
140+
141+
/// <summary>
142+
/// Checks if an asset is a NIF file based on its path and tags.
143+
/// </summary>
144+
private bool IsNifAsset(string path, string tags) {
145+
if (string.IsNullOrEmpty(path)) {
146+
return false;
147+
}
148+
149+
// Check file extension
150+
if (path.EndsWith(".nif", StringComparison.OrdinalIgnoreCase)) {
151+
return true;
152+
}
153+
154+
// Check tags for gamebryo-scenegraph (NIF format indicator)
155+
if (!string.IsNullOrEmpty(tags) && tags.Contains("gamebryo-scenegraph")) {
156+
return true;
157+
}
158+
159+
return false;
160+
}
161+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Maple2.File.Parser.Flat;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
4+
namespace Maple2.File.Tests;
5+
6+
[TestClass]
7+
public class ModelToNifResolverTest {
8+
private static ModelToNifResolver? resolver;
9+
10+
[ClassInitialize]
11+
public static void ClassInitialize(TestContext context) {
12+
resolver = new ModelToNifResolver(
13+
TestUtils.ExportedReader,
14+
TestUtils.AssetMetadataReader,
15+
"flat"
16+
);
17+
}
18+
19+
[TestMethod]
20+
public void Test_tr_floor_brick_B01() => TestModel("tr_floor_brick_B01_");
21+
22+
[TestMethod]
23+
public void Test_tr_floor_brick_C01() => TestModel("tr_floor_brick_C01_");
24+
25+
[TestMethod]
26+
public void Test_he_ground_rock_A05() => TestModel("he_ground_rock_A05_");
27+
28+
[TestMethod]
29+
public void Test_tr_deform_brick_C01() => TestModel("tr_deform_brick_C01_");
30+
31+
[TestMethod]
32+
public void Test_co_fluid_water_B02() => TestModel("co_fluid_water_B02_");
33+
34+
[TestMethod]
35+
public void Test_tr_floor_brick_B03() => TestModel("tr_floor_brick_B03_");
36+
37+
[TestMethod]
38+
public void Test_li_deform_portal_A03() => TestModel("li_deform_portal_A03_");
39+
40+
[TestMethod]
41+
public void Test_he_ground_rock_B02() => TestModel("he_ground_rock_B02_");
42+
43+
[TestMethod]
44+
public void Test_he_deform_portal_A03() => TestModel("he_deform_portal_A03_");
45+
46+
[TestMethod]
47+
public void Test_ic_fi_nature_tree_I02() => TestModel("ic_fi_nature_tree_I02_");
48+
49+
[TestMethod]
50+
public void Test_tr_floor_asphalt_A02() => TestModel("tr_floor_asphalt_A02_");
51+
52+
[TestMethod]
53+
public void Test_tr_deform_stair_B02() => TestModel("tr_deform_stair_B02_");
54+
55+
[TestMethod]
56+
public void Test_he_ground_grass_A02() => TestModel("he_ground_grass_A02_");
57+
58+
[TestMethod]
59+
public void Test_tr_deform_brick_A03() => TestModel("tr_deform_brick_A03_");
60+
61+
private void TestModel(string modelName) {
62+
string? nifAssetName = resolver?.GetNifAssetName(modelName);
63+
64+
Assert.IsNotNull(nifAssetName, $"NIF asset not found for model: {modelName}");
65+
Assert.IsFalse(string.IsNullOrEmpty(nifAssetName), $"NIF asset name is empty for model: {modelName}");
66+
67+
Console.WriteLine($"Model: {modelName} -> NIF Asset: {nifAssetName}");
68+
69+
// Get full info for additional validation
70+
(string? name, string? path, string? tags) = resolver?.GetNifAssetInfo(modelName) ?? (null, null, null);
71+
Assert.IsNotNull(path, $"NIF asset path not found for model: {modelName}");
72+
Assert.IsTrue(
73+
path!.EndsWith(".nif", StringComparison.OrdinalIgnoreCase) ||
74+
(tags != null && tags.Contains("gamebryo-scenegraph")),
75+
$"Asset for {modelName} is not a NIF file. Path: {path}, Tags: {tags}"
76+
);
77+
}
78+
79+
80+
[TestMethod]
81+
public void TestInvalidModelName() {
82+
// Test with an invalid model name
83+
string? nifAssetName = resolver?.GetNifAssetName("this_model_does_not_exist_12345");
84+
85+
Assert.IsNull(nifAssetName);
86+
}
87+
}

Maple2.File.Unity/Maple2.File.Unity.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
<!-- Flat types -->
2525
<Compile Include="..\Maple2.File.Parser\Flat\*.cs" Link="Flat\%(Filename)%(Extension)" />
2626

27+
<!-- Flat Convert (needed for AssetIndex) -->
28+
<Compile Include="..\Maple2.File.Parser\Flat\Convert\AssetIndex.cs" Link="Flat\Convert\AssetIndex.cs" />
29+
2730
<!-- MapXBlock -->
2831
<Compile Include="..\Maple2.File.Parser\MapXBlock\*.cs" Link="MapXBlock\%(Filename)%(Extension)" />
2932
<Compile Include="..\Maple2.File.Parser\MapXBlock\Generator\*.cs" Link="MapXBlock\Generator\%(Filename)%(Extension)" />

0 commit comments

Comments
 (0)