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

MeltyPlayer / MeltyTool / 19622977055

24 Nov 2025 04:05AM UTC coverage: 41.989% (+2.1%) from 39.89%
19622977055

push

github

MeltyPlayer
Switched float precision to fix broken tests.

6747 of 18131 branches covered (37.21%)

Branch coverage included in aggregate %.

28639 of 66144 relevant lines covered (43.3%)

65384.8 hits per line

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

90.97
/FinModelUtility/Libraries/JSystem/JSystem/src/api/BmdModelImporter.cs
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.Linq;
5

6
using fin;
7
using fin.animation.keyframes;
8
using fin.io;
9
using fin.log;
10
using fin.math.matrix.four;
11
using fin.math.transform;
12
using fin.model;
13
using fin.model.impl;
14
using fin.model.io.importers;
15
using fin.model.util;
16
using fin.schema.matrix;
17
using fin.util.asserts;
18

19
using gx;
20
using gx.displayList;
21

22
using jsystem._3D_Formats;
23
using jsystem.exporter;
24
using jsystem.GCN;
25
using jsystem.schema.j3dgraph.bcx;
26
using jsystem.schema.j3dgraph.bmd.inf1;
27
using jsystem.schema.j3dgraph.bmd.jnt1;
28
using jsystem.schema.j3dgraph.bmd.mat3;
29
using jsystem.schema.jutility.bti;
30

31
using schema.binary;
32

33

34
namespace jsystem.api;
35

36
using MkdsNode = MA.Node;
37

38
public sealed class BmdModelImporter : IModelImporter<BmdModelFileBundle> {
39
  public IModel Import(BmdModelFileBundle modelFileBundle) {
10✔
40
    var logger = Logging.Create<BmdModelImporter>();
10✔
41

42
    var bmd = new BMD(modelFileBundle.BmdFile.ReadAllBytes());
10✔
43

44
    List<(string, IBcx)>? pathsAndBcxs;
45
    try {
10✔
46
      pathsAndBcxs =
10!
47
          modelFileBundle
10✔
48
              .BcxFiles?
10✔
49
              .Select(bcxFile => {
56✔
50
                var extension = bcxFile.FileType.ToLower();
56✔
51
                IBcx bcx = extension switch {
56!
52
                    ".bca" => new Bca(bcxFile.ReadAllBytes()),
25✔
53
                    ".bck" => new Bck(bcxFile.ReadAllBytes()),
31✔
54
                    _      => throw new NotSupportedException(),
×
55
                };
56✔
56
                return (FullName: bcxFile.FullPath, bcx);
56✔
57
              })
56✔
58
              .ToList();
10✔
59
    } catch {
10✔
60
      logger.LogError("Failed to load BCX!");
×
61
      throw;
×
62
    }
63

64
    List<(string, Bti)>? pathsAndBtis;
65
    try {
10✔
66
      pathsAndBtis =
10!
67
          modelFileBundle
10✔
68
              .BtiFiles?
10✔
69
              .Select(btiFile
10✔
70
                          => (FullName: btiFile.FullPath,
3✔
71
                              btiFile.ReadNew<Bti>(
3✔
72
                                  Endianness.BigEndian)))
3✔
73
              .ToList();
10✔
74
    } catch {
10✔
75
      logger.LogError("Failed to load BTI!");
×
76
      throw;
×
77
    }
78

79
    var model = new ModelImpl {
10✔
80
        FileBundle = modelFileBundle,
10✔
81
        Files = modelFileBundle.Files.ToHashSet()
10✔
82
    };
10✔
83

84
    var materialManager =
10✔
85
        new BmdMaterialManager(model, bmd, pathsAndBtis);
10✔
86

87
    var jointsAndBones = this.ConvertBones_(model, bmd);
10✔
88
    this.ConvertAnimations_(model,
10✔
89
                            bmd,
10✔
90
                            pathsAndBcxs,
10✔
91
                            modelFileBundle.FrameRate,
10✔
92
                            jointsAndBones);
10✔
93
    this.ConvertMesh_(model, bmd, jointsAndBones, materialManager);
10✔
94

95
    return model;
10✔
96
  }
10✔
97

98
  private (MkdsNode, IBone)[] ConvertBones_(IModel model, BMD bmd) {
10✔
99
    var joints = bmd.GetJoints();
10✔
100

101
    var jointsAndBones = new (MkdsNode, IBone)[joints.Length];
10✔
102
    var jointIdToBone = new Dictionary<int, IBone>();
10✔
103

104
    for (var j = 0; j < joints.Length; ++j) {
347✔
105
      var node = joints[j];
109✔
106

107
      var parentBone = node.ParentJointIndex == -1
109✔
108
          ? model.Skeleton.Root
109✔
109
          : jointIdToBone[node.ParentJointIndex];
109✔
110

111
      var joint = node.Entry;
109✔
112
      var jointName = node.Name;
109✔
113

114
      var rotationFactor = 1f / 32768f * 3.14159f;
109✔
115
      var bone = parentBone.AddChild(joint.Translation);
109✔
116
      bone.LocalTransform.SetRotationRadians(
109✔
117
          joint.Rotation.X * rotationFactor,
109✔
118
          joint.Rotation.Y * rotationFactor,
109✔
119
          joint.Rotation.Z * rotationFactor);
109✔
120
      bone.LocalTransform.SetScale(joint.Scale);
109✔
121
      bone.Name = jointName;
109✔
122

123
      bone.IgnoreParentScale = node.Entry.IgnoreParentScale;
109✔
124

125
      // TODO: How to do this without hardcoding???
126
      if (node.Entry.JointType == JointType.MANUAL) {
123✔
127
        if (node.Name.StartsWith("balloon")) {
14!
128
          bone.AlwaysFaceTowardsCamera(FaceTowardsCameraType.YAW_AND_PITCH);
×
129
        }
×
130

131
        // Japanese word for light
132
        if (node.Name.StartsWith("hikair") ||
14!
133
            node.Name.StartsWith("hikari")) {
14✔
134
          bone.AlwaysFaceTowardsCamera(FaceTowardsCameraType.YAW_AND_PITCH);
×
135
        }
×
136
      }
14✔
137

138
      jointsAndBones[j] = (node, bone);
109✔
139
      jointIdToBone[j] = bone;
109✔
140
    }
109✔
141

142
    return jointsAndBones;
10✔
143
  }
10✔
144

145
  private void ConvertAnimations_(
146
      IModel model,
147
      BMD bmd,
148
      IList<(string, IBcx)>? pathsAndBcxs,
149
      float frameRate,
150
      (MkdsNode, IBone)[] jointsAndBones) {
10✔
151
    var bcxCount = pathsAndBcxs?.Count ?? 0;
10!
152
    for (var a = 0; a < bcxCount; ++a) {
188✔
153
      var (bcxPath, bcx) = pathsAndBcxs![a];
56✔
154
      var animationName = new FileInfo(bcxPath).Name.Split('.')[0];
56✔
155

156
      var animation = model.AnimationManager.AddAnimation();
56✔
157
      animation.Name = animationName;
56✔
158

159
      animation.FrameCount = bcx.Anx1.FrameCount;
56✔
160
      animation.FrameRate = frameRate;
56✔
161

162
      // Writes translation/rotation/scale for each joint.
163
      foreach (var (joint, bone) in jointsAndBones) {
3,084✔
164
        var jointIndex = bmd.JNT1.Data.StringTable[joint.Name];
972✔
165

166
        if (FinConstants.ALLOW_INVALID_JOINT_INDICES &&
972!
167
            (jointIndex < 0 || jointIndex >= bcx.Anx1.Joints.Length)) {
972✔
168
          // TODO: What does this mean???
169
          continue;
×
170
        }
171

172
        var bcxJoint = bcx.Anx1.Joints[jointIndex];
972✔
173

174
        var boneTracks = animation.GetOrCreateBoneTracks(bone);
972✔
175

176
        // TODO: Handle mirrored animations
177
        var positions = boneTracks.UseSeparateTranslationKeyframesWithTangents(
972✔
178
            bcxJoint.Values.Translations[0].Length,
972✔
179
            bcxJoint.Values.Translations[1].Length,
972✔
180
            bcxJoint.Values.Translations[2].Length);
972✔
181
        for (var i = 0; i < bcxJoint.Values.Translations.Length; ++i) {
10,692✔
182
          foreach (var key in bcxJoint.Values.Translations[i]) {
47,136✔
183
            if (key is Bck.ANK1Section.AnimatedJoint.JointAnim.Key bckKey) {
15,674✔
184
              positions.Axes[i]
2,878✔
185
                       .Add(new KeyframeWithTangents<float>(
2,878✔
186
                                bckKey.Frame,
2,878✔
187
                                bckKey.Value,
2,878✔
188
                                bckKey.IncomingTangent,
2,878✔
189
                                bckKey.OutgoingTangent));
2,878✔
190
            } else {
12,796✔
191
              positions.Axes[i]
9,918✔
192
                       .Add(new KeyframeWithTangents<float>(
9,918✔
193
                                key.Frame,
9,918✔
194
                                key.Value));
9,918✔
195
            }
9,918✔
196
          }
12,796✔
197
        }
2,916✔
198

199
        var rotations = boneTracks.UseSeparateEulerRadiansKeyframesWithTangents(
972✔
200
            bcxJoint.Values.Rotations[0].Length,
972✔
201
            bcxJoint.Values.Rotations[1].Length,
972✔
202
            bcxJoint.Values.Rotations[2].Length);
972✔
203
        for (var i = 0; i < bcxJoint.Values.Rotations.Length; ++i) {
10,692✔
204
          foreach (var key in bcxJoint.Values.Rotations[i]) {
192,954✔
205
            if (key is Bck.ANK1Section.AnimatedJoint.JointAnim.Key bckKey) {
73,544✔
206
              rotations.Axes[i]
12,142✔
207
                       .SetKeyframe(bckKey.Frame,
12,142✔
208
                                    bckKey.Value,
12,142✔
209
                                    bckKey.IncomingTangent,
12,142✔
210
                                    bckKey.OutgoingTangent);
12,142✔
211
            } else {
61,402✔
212
              rotations.Axes[i]
49,260✔
213
                       .Add(new KeyframeWithTangents<float>(
49,260✔
214
                                key.Frame,
49,260✔
215
                                key.Value));
49,260✔
216
            }
49,260✔
217
          }
61,402✔
218
        }
2,916✔
219

220
        var scales = boneTracks.UseSeparateScaleKeyframesWithTangents(
972✔
221
            bcxJoint.Values.Scales[0].Length,
972✔
222
            bcxJoint.Values.Scales[1].Length,
972✔
223
            bcxJoint.Values.Scales[2].Length);
972✔
224
        for (var i = 0; i < bcxJoint.Values.Scales.Length; ++i) {
10,692✔
225
          foreach (var key in bcxJoint.Values.Scales[i]) {
35,037✔
226
            if (key is Bck.ANK1Section.AnimatedJoint.JointAnim.Key bckKey) {
10,881✔
227
              scales.Axes[i]
2,118✔
228
                    .Add(new KeyframeWithTangents<float>(
2,118✔
229
                             bckKey.Frame,
2,118✔
230
                             bckKey.Value,
2,118✔
231
                             bckKey.IncomingTangent,
2,118✔
232
                             bckKey.OutgoingTangent));
2,118✔
233
            } else {
8,763✔
234
              scales.Axes[i]
6,645✔
235
                    .Add(new KeyframeWithTangents<float>(
6,645✔
236
                             key.Frame,
6,645✔
237
                             key.Value));
6,645✔
238
            }
6,645✔
239
          }
8,763✔
240
        }
2,916✔
241
      }
972✔
242
    }
56✔
243
  }
10✔
244

245
  private void ConvertMesh_(
246
      ModelImpl model,
247
      BMD bmd,
248
      (MkdsNode, IBone)[] jointsAndBones,
249
      BmdMaterialManager materialManager) {
10✔
250
    var finSkin = model.Skin;
10✔
251

252
    var joints = bmd.GetJoints();
10✔
253

254
    var vertexPositions = bmd.VTX1.Positions;
10✔
255
    var vertexNormals = bmd.VTX1.Normals;
10✔
256
    var vertexColors = bmd.VTX1.Colors;
10✔
257
    var vertexUvs = bmd.VTX1.TexCoords;
10✔
258
    var entries = bmd.INF1.Data.Entries;
10✔
259
    var batches = bmd.SHP1.Batches;
10✔
260

261
    var scheduledDrawOnWayDownPrimitives = new List<IPrimitive>();
10✔
262
    var scheduledDrawOnWayUpPrimitives = new List<IPrimitive>();
10✔
263

264
    GxFixedFunctionMaterial? currentMaterial = null;
10✔
265
    MaterialEntry? currentMaterialEntry = null;
10✔
266

267
    uint currentRenderIndex = 1;
10✔
268

269
    var weightsTable = new IBoneWeights?[10];
10✔
270
    foreach (var entry in entries) {
2,585✔
271
      switch (entry.Type) {
855✔
272
        case Inf1EntryType.TERMINATOR:
273
          goto DoneRendering;
10✔
274

275
        case Inf1EntryType.HIERARCHY_DOWN: {
263✔
276
          foreach (var primitive in scheduledDrawOnWayDownPrimitives) {
55,923✔
277
            primitive.SetInversePriority(currentRenderIndex++);
18,378✔
278
          }
18,378✔
279

280
          scheduledDrawOnWayDownPrimitives.Clear();
263✔
281
          break;
263✔
282
        }
283

284
        case Inf1EntryType.HIERARCHY_UP: {
263✔
285
          foreach (var primitive in scheduledDrawOnWayUpPrimitives) {
1,953✔
286
            primitive.SetInversePriority(currentRenderIndex++);
388✔
287
          }
388✔
288

289
          scheduledDrawOnWayUpPrimitives.Clear();
263✔
290
          break;
263✔
291
        }
292

293
        case Inf1EntryType.MATERIAL:
294
          currentMaterial = materialManager.Get(entry.Index);
105✔
295
          currentMaterialEntry =
105✔
296
              bmd.MAT3.MaterialEntries[
105✔
297
                  bmd.MAT3.MaterialEntryIndieces[entry.Index]];
105✔
298
          break;
105✔
299

300
        case Inf1EntryType.SHAPE:
301
          var batchIndex = entry.Index;
105✔
302
          var batch = batches[batchIndex];
105✔
303

304
          var finMesh = finSkin.AddMesh();
105✔
305
          finMesh.Name = $"batch {batchIndex}";
105✔
306

307
          // TODO: Pass matrix type into joint (how?)
308
          // TODO: Implement this instead of hardcoding billboards for Pikmin 2
309
          var matrixType = batch.MatrixType;
105✔
310

311
          foreach (var packet in batch.Packets) {
645✔
312
            // Updates contents of matrix table
313
            for (var i = 0; i < packet.MatrixTable.Length; ++i) {
826✔
314
              var matrixTableIndex = packet.MatrixTable[i];
202✔
315

316
              // Max value means keep old value.
317
              if (matrixTableIndex == ushort.MaxValue) {
215✔
318
                continue;
13✔
319
              }
320

321
              var drw1 = bmd.DRW1.Data;
189✔
322
              var isWeighted = drw1.IsWeighted[matrixTableIndex];
189✔
323
              var drw1Index = drw1.Data[matrixTableIndex];
189✔
324

325
              BoneWeight[] weights;
326
              if (isWeighted) {
203✔
327
                var weightedIndices = bmd.EVP1.Data.WeightedIndices[drw1Index];
14✔
328
                weights = new BoneWeight[weightedIndices.Indices.Length];
14✔
329
                for (var w = 0; w < weightedIndices.Indices.Length; ++w) {
112✔
330
                  var jointIndex = weightedIndices.Indices[w];
28✔
331
                  var weight = weightedIndices.Weights[w];
28✔
332

333
                  if (jointIndex >= joints.Length) {
28!
334
                    throw new InvalidDataException();
×
335
                  }
336

337
                  var skinToBoneMatrix =
28✔
338
                      ConvertSchemaToFin_(
28✔
339
                          bmd.EVP1.Data.InverseBindMatrices[jointIndex]);
28✔
340

341
                  var bone = jointsAndBones[jointIndex].Item2;
28✔
342
                  weights[w] = new BoneWeight(bone, skinToBoneMatrix, weight);
28✔
343
                }
28✔
344
              }
14✔
345
              // Unweighted bones are simple, just gets our precomputed limb
346
              // matrix
347
              else {
175✔
348
                var jointIndex = drw1Index;
175✔
349
                if (jointIndex >= joints.Length) {
175!
350
                  throw new InvalidDataException();
×
351
                }
352

353
                var bone = jointsAndBones[jointIndex].Item2;
175✔
354
                weights = [
175✔
355
                    new BoneWeight(bone, FinMatrix4x4Util.IDENTITY, 1)
175✔
356
                ];
175✔
357
              }
175✔
358

359
              weightsTable[i] =
189✔
360
                  finSkin.GetOrCreateBoneWeights(
189✔
361
                      VertexSpace.RELATIVE_TO_BONE,
189✔
362
                      weights);
189✔
363
            }
189✔
364

365
            foreach (var primitive in packet.Primitives) {
56,955✔
366
              var points = primitive.Points;
18,875✔
367
              var pointsCount = points.Length;
18,875✔
368
              var vertices = new IVertex[pointsCount];
18,875✔
369

370
              for (var p = 0; p < pointsCount; ++p) {
330,283✔
371
                var point = points[p];
97,511✔
372

373
                var position = vertexPositions[point.PosIndex];
97,511✔
374
                var vertex =
97,511✔
375
                    finSkin.AddVertex(position.X, position.Y, position.Z);
97,511✔
376
                vertices[p] = vertex;
97,511✔
377

378
                var normalIndex = point.NormalIndex;
97,511✔
379
                if (normalIndex != null) {
145,387✔
380
                  vertex.SetLocalNormal(vertexNormals[normalIndex.Value]);
47,876✔
381
                }
47,876✔
382

383
                var matrixIndex = point.MatrixIndex;
97,511✔
384
                if (matrixIndex != null) {
195,022✔
385
                  var weights = weightsTable[matrixIndex];
97,511✔
386
                  if (weights != null) {
195,022✔
387
                    vertex.SetBoneWeights(weights);
97,511✔
388
                  }
97,511✔
389
                }
97,511✔
390

391
                var colorIndices = point.ColorIndices;
97,511✔
392
                for (var c = 0; c < 2; ++c) {
780,088✔
393
                  var colorIndex = colorIndices[c];
195,022✔
394
                  if (colorIndex != null) {
281,407✔
395
                    var color = vertexColors[c][colorIndex.Value];
86,385✔
396
                    vertex.SetColorBytes(c,
86,385✔
397
                                         color.Rb,
86,385✔
398
                                         color.Gb,
86,385✔
399
                                         color.Bb,
86,385✔
400
                                         color.Ab);
86,385✔
401
                  }
86,385✔
402
                }
195,022✔
403

404
                var texCoordIndices = point.TexCoordIndices;
97,511✔
405
                for (var i = 0; i < 8; ++i) {
2,535,286✔
406
                  var texCoordIndex = texCoordIndices[i];
780,088✔
407
                  if (texCoordIndex != null) {
919,383✔
408
                    vertex.SetUv(i, vertexUvs[i][texCoordIndex.Value]);
139,295✔
409
                  }
139,295✔
410
                }
780,088✔
411
              }
97,511✔
412

413
              var gxPrimitiveType = primitive.Type;
18,875✔
414

415
              Asserts.Nonnull(currentMaterial);
18,875✔
416

417
              var finPrimitive = gxPrimitiveType switch {
18,875!
418
                  GxPrimitiveType.GX_TRIANGLES
18,875✔
419
                      => finMesh.AddTriangles(vertices),
×
420
                  GxPrimitiveType.GX_TRIANGLE_STRIP
18,875✔
421
                      => finMesh.AddTriangleStrip(vertices),
18,875✔
422
                  GxPrimitiveType.GX_TRIANGLE_FAN
18,875✔
423
                      => finMesh.AddTriangleFan(vertices),
×
424
                  GxPrimitiveType.GX_QUADS
18,875✔
425
                      => finMesh.AddQuads(vertices),
×
426
                  _ => throw new NotSupportedException(
×
427
                      $"Unsupported primitive type: {gxPrimitiveType}")
×
428
              };
18,875✔
429

430
              finPrimitive.SetMaterial(currentMaterial.Material);
18,875✔
431

432
              var renderOrder = currentMaterialEntry?.RenderOrder ??
18,875!
433
                                RenderOrder.DRAW_ON_WAY_DOWN;
18,875✔
434
              switch (renderOrder) {
18,875!
435
                case RenderOrder.DRAW_ON_WAY_DOWN: {
18,487✔
436
                  scheduledDrawOnWayDownPrimitives.Add(finPrimitive);
18,487✔
437
                  break;
18,487✔
438
                }
439
                case RenderOrder.DRAW_ON_WAY_UP: {
388✔
440
                  scheduledDrawOnWayUpPrimitives.Add(finPrimitive);
388✔
441
                  break;
388✔
442
                }
443
                default: throw new ArgumentOutOfRangeException();
×
444
              }
445
            }
18,875✔
446
          }
110✔
447

448
          break;
105✔
449
      }
450
    }
845✔
451

452
    DoneRendering: ;
20✔
453
  }
10✔
454

455
  private static IFinMatrix4x4 ConvertSchemaToFin_(Matrix3x4f schemaMatrix) {
28✔
456
    var finMatrix = new FinMatrix4x4().SetIdentity();
28✔
457

458
    for (var r = 0; r < 3; ++r) {
308✔
459
      for (var c = 0; c < 4; ++c) {
1,176✔
460
        finMatrix[c, r] = schemaMatrix[r, c];
336✔
461
      }
336✔
462
    }
84✔
463

464
    return finMatrix;
28✔
465
  }
28✔
466
}
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