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

MeltyPlayer / MeltyTool / 21239091484

22 Jan 2026 06:55AM UTC coverage: 42.242% (-0.01%) from 42.255%
21239091484

push

github

MeltyPlayer
More fiddling w/ PLvPW.

6865 of 18402 branches covered (37.31%)

Branch coverage included in aggregate %.

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

2 existing lines in 2 files now uncovered.

29415 of 67485 relevant lines covered (43.59%)

64421.42 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

NEW
38
    var finBoneByHash = 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

NEW
97
          finBoneByHash[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

×
137
          finMaterial.Shininess = 0;
×
138

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

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

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

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

155
          var prmAnimationReferenceHashes = prm.AnimationReferenceHashes;
×
156
          foreach (var hash in prmAnimationReferenceHashes) {
×
NEW
157
            prmsByAnimationReferenceHash.Add(hash, mesh);
×
158
          }
×
159

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

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

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

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

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

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

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

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

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

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

216
            var anim = mtn2.Anim;
×
217

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

NEW
223
            var meshesAlreadyTouched = new HashSet<IMesh>();
×
224
            foreach (var (animationReferenceHash, framesAndValues) in mtn2
×
225
                         .Somethings.GetPairs()) {
×
NEW
226
              if (!prmsByAnimationReferenceHash.TryGetList(
×
227
                      animationReferenceHash,
×
NEW
228
                      out var meshes)) {
×
229
                continue;
×
230
              }
231

NEW
232
              if (meshes.Count > 1) {
×
NEW
233
                ;
×
NEW
234
              }
×
235

NEW
236
              foreach (var mesh in meshes) {
×
NEW
237
                if (!meshesAlreadyTouched.Add(mesh)) {
×
NEW
238
                  ;
×
UNCOV
239
                }
×
240

NEW
241
                var meshTracks = finAnimation.AddMeshTracks(mesh);
×
NEW
242
                var displayStates = meshTracks.DisplayStates;
×
243

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

NEW
255
                  var (frame, value) = frameAndValue;
×
256

257
                  // TODO: This is just a guess, but still doesn't look right.
NEW
258
                  var displayState = (MeshDisplayState?) null;
×
NEW
259
                  if (value == 1) {
×
NEW
260
                    displayState = MeshDisplayState.VISIBLE;
×
NEW
261
                  } else if (value == 0) {
×
NEW
262
                    displayState = MeshDisplayState.HIDDEN;
×
NEW
263
                  }
×
264

NEW
265
                  if (displayState != null) {
×
NEW
266
                    displayStates.SetKeyframe(frame, displayState.Value);
×
NEW
267
                  }
×
268
                }
×
269
              }
×
270
            }
×
271

272
            foreach (var transformNode in anim.TransformNodes) {
×
NEW
273
              if (!finBoneByHash.TryGetValue(transformNode.Hash,
×
274
                                              out var finBone)) {
×
275
                continue;
×
276
              }
277

278
              var finBoneTracks = finAnimation.GetOrCreateBoneTracks(finBone);
×
279
              var positions = finBoneTracks
×
280
                  .UseSeparateTranslationKeyframesWithTangents();
×
281
              var rotations = finBoneTracks
×
282
                  .UseSeparateEulerRadiansKeyframesWithTangents();
×
283
              var scales =
×
284
                  finBoneTracks.UseSeparateScaleKeyframesWithTangents();
×
285

286
              foreach (var mtnTrack in transformNode.Tracks.Values) {
×
287
                foreach (var mtnKey in mtnTrack.Keys.Keys) {
×
288
                  var frame = (int) mtnKey.Frame;
×
289
                  var value = mtnKey.Value;
×
290

291
                  var inTan = mtnKey.InTan;
×
292
                  var outTan = mtnKey.OutTan;
×
293

294
                  if (mtnTrack.Type.IsTranslation(out var translationAxis)) {
×
295
                    positions.Axes[translationAxis]
×
296
                             .Add(new KeyframeWithTangents<float>(
×
297
                                      frame,
×
298
                                      value,
×
299
                                      inTan,
×
300
                                      outTan));
×
301
                  } else if (mtnTrack.Type.IsRotation(out var rotationAxis)) {
×
302
                    rotations.Axes[rotationAxis]
×
303
                             .SetKeyframe(
×
304
                                 frame,
×
305
                                 value,
×
306
                                 inTan,
×
307
                                 outTan);
×
308
                  } else if (mtnTrack.Type.IsScale(out var scaleAxis)) {
×
309
                    scales.Axes[scaleAxis]
×
310
                          .Add(new KeyframeWithTangents<float>(frame,
×
311
                                 value,
×
312
                                 inTan,
×
313
                                 outTan));
×
314
                  } else {
×
315
                    throw new NotSupportedException();
×
316
                  }
317
                }
×
318
              }
×
319
            }
×
320
          }
×
321
        }
×
322
      }
×
323
    }
×
324

325
    return model;
×
326
  }
×
327
}
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