• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MeltyPlayer / MeltyTool / 21238229252

22 Jan 2026 06:15AM UTC coverage: 42.255% (-0.001%) from 42.256%
21238229252

push

github

MeltyPlayer
Documented some stuff in PLvPW.

6867 of 18394 branches covered (37.33%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 2 files covered. (0.0%)

2 existing lines in 2 files now uncovered.

29415 of 67470 relevant lines covered (43.6%)

64435.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/FinModelUtility/Libraries/Level5/Level5/src/api/XcModelImporter.cs
1
using fin.animation.keyframes;
2
using fin.data.dictionaries;
3
using fin.data.lazy;
4
using fin.data.nodes;
5
using fin.data.queues;
6
using fin.io;
7
using fin.math.rotations;
8
using fin.math.transform;
9
using fin.model;
10
using fin.model.impl;
11
using fin.model.io.importers;
12
using fin.model.util;
13

14
using level5.schema;
15

16
using OpenTK.Mathematics;
17

18
using schema.binary;
19

20
using Quaternion = System.Numerics.Quaternion;
21

22

23
namespace level5.api;
24

25
public sealed class XcModelImporter : IModelImporter<XcModelFileBundle> {
26
  public IModel Import(XcModelFileBundle modelFileBundle) {
×
27
    var endianness = Endianness.LittleEndian;
×
28

29
    var modelDirectory = modelFileBundle.ModelDirectory;
×
30
    var modelResourceFile =
×
31
        new Resource(modelDirectory.GetFilesWithFileType(".bin").Single());
×
32

33
    var model = new ModelImpl {
×
34
        FileBundle = modelFileBundle,
×
35
        Files = modelFileBundle.Files.ToHashSet()
×
36
    };
×
37

38
    var finBoneByIndex = new Dictionary<uint, IBone>();
×
39
    var finBoneByName = new Dictionary<string, IBone>();
×
40
    {
×
41
      var mbnFiles = modelDirectory.GetFilesWithFileType(".mbn").ToArray();
×
42
      if (mbnFiles.Any()) {
×
43
        var mbns = mbnFiles
×
44
                   .Select(mbnFile => mbnFile.ReadNew<Mbn>(endianness))
×
45
                   .ToArray();
×
46
        var mbnNodeList =
×
47
            mbns.Where(mbn => mbn.Id != mbn.ParentId)
×
48
                .DistinctBy(mbn => mbn.Id)
×
49
                .Select(mbn => new TreeNode<Mbn> {Value = mbn})
×
50
                .ToArray();
×
51
        var mbnByIndex =
×
52
            mbnNodeList.ToDictionary(node => node.Value.Id);
×
53

54
        foreach (var mbnNode in mbnNodeList) {
×
55
          var mbn = mbnNode.Value;
×
56
          if (mbn.ParentId == 0) {
×
57
            continue;
×
58
          }
59

60
          mbnNode.Parent = mbnByIndex[mbn.ParentId];
×
61
        }
×
62

63
        var rootMbnNodes =
×
64
            mbnNodeList.Where(node => node.Value.ParentId == 0);
×
65

66
        var mbnQueue =
×
67
            new FinTuple2Queue<ITreeNode<Mbn>, IBone>(
×
68
                rootMbnNodes.Select(
×
69
                    node => ((ITreeNode<Mbn>) node, model.Skeleton.Root)));
×
70
        while (mbnQueue.TryDequeue(out var mbnNode, out var parentBone)) {
×
71
          var mbn = mbnNode.Value;
×
72

73
          var bone = parentBone.AddChild(mbn.Position);
×
74
          bone.Name = modelResourceFile.GetResourceName(mbn.Id);
×
75

76
          var mat3 = mbn.RotationMatrix3;
×
77
          var matrix = new Matrix3(mat3[0],
×
78
                                   mat3[1],
×
79
                                   mat3[2],
×
80
                                   mat3[3],
×
81
                                   mat3[4],
×
82
                                   mat3[5],
×
83
                                   mat3[6],
×
84
                                   mat3[7],
×
85
                                   mat3[8]);
×
86
          var openTkQuaternion = matrix.ExtractRotation();
×
87
          var quaternion = new Quaternion(openTkQuaternion.X,
×
88
                                          openTkQuaternion.Y,
×
89
                                          openTkQuaternion.Z,
×
90
                                          openTkQuaternion.W);
×
91
          var eulerRadians = QuaternionUtil.ToEulerRadians(quaternion);
×
92
          bone.Transform.SetRotationRadians(eulerRadians);
×
93

94
          var scale = mbn.Scale;
×
95
          bone.Transform.SetScale(scale);
×
96

97
          finBoneByIndex[mbn.Id] = bone;
×
98
          finBoneByName[bone.Name] = bone;
×
99

100
          mbnQueue.Enqueue(
×
101
              mbnNode.ChildNodes.Select(childNode => (childNode, bone)));
×
102
        }
×
103
      }
×
104
    }
×
105

106
    var xiFiles = modelDirectory.GetFilesWithFileType(".xi").ToArray();
×
107
    var lazyTextures = new LazyCaseInvariantStringDictionary<ITexture>(
×
108
        textureName => {
×
109
          var textureIndex =
×
110
              modelResourceFile.TextureNames.IndexOf(textureName);
×
111
          var xiFile = xiFiles[textureIndex];
×
112

×
113
          var xi = new Xi();
×
114
          xi.Open(xiFile);
×
115

×
116
          var image = xi.ToBitmap();
×
117
          var texture = model.MaterialManager.CreateTexture(image);
×
118
          texture.Name = textureName;
×
119

×
120
          return texture;
×
121
        });
×
122

123
    var lazyMaterials = new LazyCaseInvariantStringDictionary<IMaterial>(
×
124
        materialName => {
×
125
          var binMaterial =
×
126
              modelResourceFile.Materials.Single(
×
127
                  mat => mat.Name == materialName);
×
128
          var finTexture = lazyTextures[binMaterial.TexName];
×
129

×
130
          var finMaterial =
×
131
              model.MaterialManager.AddTextureMaterial(finTexture);
×
132
          finMaterial.Name = binMaterial.Name;
×
133

×
134
          // TODO: Figure out how to fix culling issues
×
135
          finMaterial.CullingMode = CullingMode.SHOW_BOTH;
×
136

×
NEW
137
          finMaterial.Shininess = 0;
×
NEW
138

×
139
          return finMaterial;
×
140
        });
×
141

142
    // Adds vertices
143
    Dictionary<uint, (Prm prm, IMesh mesh)> prmsByAnimationReferenceHash =
×
144
        new();
×
145
    {
×
146
      var prmFiles = modelDirectory.GetFilesWithFileType(".prm").ToArray();
×
147
      if (prmFiles.Any()) {
×
148
        foreach (var prmFile in prmFiles) {
×
149
          var prm = new Prm(prmFile);
×
150

151
          var finMaterial = lazyMaterials[prm.MaterialName];
×
152

153
          var mesh = model.Skin.AddMesh();
×
154
          mesh.Name = prm.Name;
×
155

156
          var prmAnimationReferenceHashes = prm.AnimationReferenceHashes;
×
157
          foreach (var hash in prmAnimationReferenceHashes) {
×
158
            prmsByAnimationReferenceHash[hash] = (prm, mesh);
×
159
          }
×
160

161
          var finVertices = new List<IVertex>();
×
162
          foreach (var prmVertex in prm.Vertices) {
×
163
            var position = prmVertex.Pos;
×
164
            var finVertex =
×
165
                model.Skin.AddVertex(position.X, position.Y, position.Z);
×
166

167
            var uv = prmVertex.Uv0;
×
168
            finVertex.SetUv(uv.X, uv.Y);
×
169

170
            var normal = prmVertex.Nrm;
×
171
            finVertex.SetLocalNormal(normal.X, normal.Y, normal.Z);
×
172

173
            var prmBones = prmVertex.Bones;
×
174
            if (prmBones != null) {
×
175
              var boneWeightList = new List<BoneWeight>();
×
176
              for (var b = 0; b < 4; ++b) {
×
177
                var boneId = prmVertex.Bones[b];
×
178
                var weight = prmVertex.Weights[b];
×
179

180
                var finBone = finBoneByIndex[boneId];
×
181
                boneWeightList.Add(new BoneWeight(finBone, null, weight));
×
182
              }
×
183

184
              var boneWeights =
×
185
                  model.Skin.GetOrCreateBoneWeights(
×
186
                      VertexSpace.RELATIVE_TO_BONE,
×
187
                      boneWeightList.ToArray());
×
188
              finVertex.SetBoneWeights(boneWeights);
×
189
            }
×
190

191
            finVertices.Add(finVertex);
×
192
          }
×
193

194
          var finTriangleVertices = prm.Triangles
×
195
                                       .Select(vertexIndex =>
×
196
                                                   finVertices[
×
197
                                                       (int) vertexIndex])
×
198
                                       .ToArray();
×
199
          var triangles = mesh.AddTriangles(finTriangleVertices);
×
200
          triangles.SetMaterial(finMaterial);
×
201
        }
×
202
      }
×
203
    }
×
204

205
    // Adds animations
206
    {
×
207
      foreach (var animationDirectory in modelFileBundle.AnimationDirectories) {
×
208
        var binFile = animationDirectory.GetFilesWithFileType(".bin").Single();
×
209
        var animationResourceFile = new Resource(binFile);
×
210

211
        var mtn2Files = animationDirectory.GetFilesWithFileType(".mtn2").ToArray();
×
212
        if (mtn2Files.Any()) {
×
213
          foreach (var mtn2File in mtn2Files) {
×
214
            var mtn2 = new Mtn2();
×
215
            mtn2.Open(mtn2File);
×
216

217
            var anim = mtn2.Anim;
×
218

219
            var finAnimation = model.AnimationManager.AddAnimation();
×
220
            finAnimation.Name = anim.Name;
×
221
            finAnimation.FrameRate = 30;
×
222
            finAnimation.FrameCount = anim.FrameCount;
×
223

224
            foreach (var (animationReferenceHash, framesAndValues) in mtn2
×
225
                         .Somethings.GetPairs()) {
×
226
              if (!prmsByAnimationReferenceHash.TryGetValue(
×
227
                      animationReferenceHash,
×
228
                      out var prmAndMesh)) {
×
229
                continue;
×
230
              }
231

232
              var (prm, mesh) = prmAndMesh;
×
233
              var meshTracks = finAnimation.AddMeshTracks(mesh);
×
234
              var displayStates = meshTracks.DisplayStates;
×
235

236
              foreach (var frameAndValue in framesAndValues) {
×
237
                // TODO: Still not clear what the hash is meant to be,
238
                // sometimes the thing above fails to match. It's also possible
239
                // for this to target a bone, which is not explicitly handled
240
                // here.
241
                // - Not sure of any other mechanism to fiddle with textures,
242
                //   i.e. selecting which eye or mouth to show. Probably done
243
                //   via these values somehow?
244
                // - Why do PRMs need multiple values? Maybe each encodes a
245
                //   different state, potentially those texture cases?
246

UNCOV
247
                var (frame, value) = frameAndValue;
×
248

249
                // TODO: This is just a guess, but still doesn't look right.
250
                var displayState = (MeshDisplayState?) null;
×
251
                if (value == 1) {
×
252
                  displayState = MeshDisplayState.VISIBLE;
×
253
                } else if (value == 0) {
×
254
                  displayState = MeshDisplayState.HIDDEN;
×
255
                }
×
256

257
                if (displayState != null) {
×
258
                  displayStates.SetKeyframe(frame, displayState.Value);
×
259
                }
×
260
              }
×
261
            }
×
262

263
            foreach (var transformNode in anim.TransformNodes) {
×
264
              if (!finBoneByIndex.TryGetValue(transformNode.Hash,
×
265
                                              out var finBone)) {
×
266
                continue;
×
267
              }
268

269
              var finBoneTracks = finAnimation.GetOrCreateBoneTracks(finBone);
×
270
              var positions = finBoneTracks
×
271
                  .UseSeparateTranslationKeyframesWithTangents();
×
272
              var rotations = finBoneTracks
×
273
                  .UseSeparateEulerRadiansKeyframesWithTangents();
×
274
              var scales =
×
275
                  finBoneTracks.UseSeparateScaleKeyframesWithTangents();
×
276

277
              foreach (var mtnTrack in transformNode.Tracks.Values) {
×
278
                foreach (var mtnKey in mtnTrack.Keys.Keys) {
×
279
                  var frame = (int) mtnKey.Frame;
×
280
                  var value = mtnKey.Value;
×
281

282
                  var inTan = mtnKey.InTan;
×
283
                  var outTan = mtnKey.OutTan;
×
284

285
                  if (mtnTrack.Type.IsTranslation(out var translationAxis)) {
×
286
                    positions.Axes[translationAxis]
×
287
                             .Add(new KeyframeWithTangents<float>(
×
288
                                      frame,
×
289
                                      value,
×
290
                                      inTan,
×
291
                                      outTan));
×
292
                  } else if (mtnTrack.Type.IsRotation(out var rotationAxis)) {
×
293
                    rotations.Axes[rotationAxis]
×
294
                             .SetKeyframe(
×
295
                                 frame,
×
296
                                 value,
×
297
                                 inTan,
×
298
                                 outTan);
×
299
                  } else if (mtnTrack.Type.IsScale(out var scaleAxis)) {
×
300
                    scales.Axes[scaleAxis]
×
301
                          .Add(new KeyframeWithTangents<float>(frame,
×
302
                                 value,
×
303
                                 inTan,
×
304
                                 outTan));
×
305
                  } else {
×
306
                    throw new NotSupportedException();
×
307
                  }
308
                }
×
309
              }
×
310
            }
×
311
          }
×
312
        }
×
313
      }
×
314
    }
×
315

316
    return model;
×
317
  }
×
318
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc