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

MeltyPlayer / MeltyTool / 28220998723

26 Jun 2026 06:19AM UTC coverage: 40.975% (-0.1%) from 41.112%
28220998723

push

github

MeltyPlayer
Updated MarioArtist goldens.

7326 of 19830 branches covered (36.94%)

Branch coverage included in aggregate %.

30582 of 72686 relevant lines covered (42.07%)

59979.76 hits per line

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

0.0
/FinModelUtility/Games/TheLabyrinthPlusEdition/TheLabyrinthPlusEdition/src/api/ScbModelImporter.cs
1
using System.Numerics;
2

3
using fin.animation.keyframes;
4
using fin.color;
5
using fin.data.dictionaries;
6
using fin.data.lazy;
7
using fin.image;
8
using fin.io;
9
using fin.math.rotations;
10
using fin.math.transform;
11
using fin.model;
12
using fin.model.impl;
13
using fin.model.io;
14
using fin.model.io.importers;
15
using fin.model.util;
16
using fin.util.sets;
17
using fin.util.strings;
18

19
using schema.text.reader;
20

21
using tlpe.scb;
22

23
namespace tlpe.api;
24

25
public sealed record ScbModelFileBundle(
×
26
    IReadOnlyTreeFile ScbFile,
×
27
    IReadOnlyTreeFile BallsFile,
×
28
    IReadOnlyTreeDirectory TexturesDir)
×
29
    : IModelFileBundle {
30
  public IReadOnlyTreeFile MainFile => this.ScbFile;
×
31
}
32

33
public sealed class ScbModelImporter : IModelImporter<ScbModelFileBundle> {
34
  public IModel Import(ScbModelFileBundle fileBundle) {
×
35
    var scb = fileBundle.ScbFile.ReadNew<Scb>();
×
36

37
    var files = fileBundle.ScbFile.AsFileSet();
×
38
    var finModel = new ModelImpl {
×
39
        FileBundle = fileBundle,
×
40
        Files = files
×
41
    };
×
42

43
    var allBallAttributes = ReadAllBallAttributes_(fileBundle.BallsFile);
×
44
    var ballAttributesByGeometry
×
45
        = allBallAttributes
×
46
            .ToListDictionary(a => a.Geometry?.ToLower() ?? "ball.scb");
×
47

48
    var material1TextureFiles = new List<IReadOnlyTreeFile>();
×
49
    if (ballAttributesByGeometry.TryGetList(
×
50
            fileBundle.ScbFile.Name.ToString().ToLower(),
×
51
            out var matchingBallAttributes)) {
×
52
      foreach (var ballAttributes in matchingBallAttributes) {
×
53
        if (ballAttributes.Texture != null) {
×
54
          var textureFile = fileBundle.TexturesDir.AssertGetExistingFile(
×
55
              ballAttributes.Texture);
×
56
          material1TextureFiles.Add(textureFile);
×
57
        }
×
58
      }
×
59
    }
×
60

61
    var materialById = new Dictionary<uint, IReadOnlyMaterial>();
×
62

63
    foreach (var scbSection in scb.Sections) {
×
64
      if (scbSection is MaterialSection materialSection) {
×
65
        var id = materialSection.Id;
×
66

67
        var textureFiles = new List<IReadOnlyTreeFile>();
×
68
        var textureName = materialSection.TextureName;
×
69
        if (textureName.Length > 0) {
×
70
          var textureFile = fileBundle.ScbFile.AssertGetParent()
×
71
                                      .AssertGetExistingFile(textureName);
×
72
          textureFiles.Add(textureFile);
×
73
        }
×
74

75
        if (id == 1) {
×
76
          textureFiles.AddRange(material1TextureFiles);
×
77
        }
×
78

79
        var textures = textureFiles
×
80
                       .Select(textureFile => {
×
81
                         var image = FinImage.FromFile(textureFile);
×
82
                         var finTexture
×
83
                             = finModel.MaterialManager.CreateTexture(image);
×
84
                         finTexture.Name
×
85
                             = textureFile.NameWithoutExtension.ToString();
×
86
                         finTexture.WrapModeU
×
87
                             = finTexture.WrapModeV = WrapMode.REPEAT;
×
88
                         return finTexture;
×
89
                       })
×
90
                       .ToArray();
×
91

92
        var finMaterial = finModel.MaterialManager.AddStandardMaterial();
×
93
        finMaterial.Name = materialSection.Name;
×
94
        finMaterial.DiffuseTexture = textures.FirstOrDefault();
×
95

96
        materialById[id] = finMaterial;
×
97
      }
×
98
    }
×
99

100
    var finSkin = finModel.Skin;
×
101

102
    IBone? currentBone = null;
×
103
    IReadOnlyBoneWeights? currentBoneWeights = null;
×
104

105
    var finBoneByName = new CaseInvariantStringDictionary<IBone>();
×
106

107
    var framerate = 30f;
×
108
    var lazyAnimationById = new LazyDictionary<uint, IModelAnimation>(i => {
×
109
      var finAnimation = finModel.AnimationManager.AddAnimation();
×
110
      finAnimation.Name = $"animation {i}";
×
111
      finAnimation.FrameRate = framerate;
×
112
      return finAnimation;
×
113
    });
×
114

115
    foreach (var scbSection in scb.Sections) {
×
116
      switch (scbSection) {
×
117
        case JointSection joint: {
×
118
          if (!finBoneByName.TryGetValue(joint.ParentName,
×
119
                                         out var parentBone)) {
×
120
            parentBone = finModel.Skeleton.Root;
×
121
          }
×
122

123
          currentBone = parentBone.AddChild(AdjustVector3_(joint.Translation));
×
124
          currentBone.Transform.SetRotation(
×
125
              ConvertRotationToQuaternion_(joint.Rotation));
×
126
          currentBone.Transform.SetScale(AdjustVector3_(joint.Scale));
×
127
          currentBone.Name = joint.Name;
×
128

129
          finBoneByName[currentBone.Name] = currentBone;
×
130

131
          currentBoneWeights
×
132
              = finModel.Skin.GetOrCreateBoneWeights(
×
133
                  VertexSpace.RELATIVE_TO_BONE,
×
134
                  currentBone);
×
135

136
          break;
×
137
        }
138
        case AnimationSection animationSection: {
×
139
          var finAnimation = lazyAnimationById[animationSection.Id];
×
140

141
          var finBoneTracks = finAnimation.GetOrCreateBoneTracks(currentBone!);
×
142
          var rotationTrack = finBoneTracks.UseCombinedQuaternionKeyframes(
×
143
              animationSection.Keyframes.Length);
×
144
          foreach (var keyframe in animationSection.Keyframes) {
×
145
            var frame = keyframe.Frame / framerate;
×
146

147
            finAnimation.FrameCount = (int) Math.Max(finAnimation.FrameCount, frame);
×
148

149
            var rotation = currentBone.Transform.LocalRotation.Value *
×
150
                           ConvertRotationToQuaternion_(keyframe.EulerRadians);
×
151
            rotationTrack.SetKeyframe(frame, rotation);
×
152
          }
×
153

154
          break;
×
155
        }
156
        case MeshSection meshSection: {
×
157
          var finMesh = finSkin.AddMesh();
×
158
          finMesh.Name = currentBone?.Name;
×
159

160

161
          var scbVertices = meshSection.Vertices;
×
162
          var finVertices = new IReadOnlyVertex[scbVertices.Length];
×
163
          for (var i = 0; i < scbVertices.Length; ++i) {
×
164
            var scbVertex = scbVertices[i];
×
165

166
            var finVertex
×
167
                = finSkin.AddVertex(AdjustVector3_(scbVertex.Position));
×
168
            finVertex.SetLocalNormal(AdjustVector3_(scbVertex.Normal));
×
169
            finVertex.SetUv(0, AdjustVector2_(scbVertex.Uv0));
×
170
            finVertex.SetUv(1, AdjustVector2_(scbVertex.Uv1));
×
171

172
            if (currentBoneWeights != null) {
×
173
              finVertex.SetBoneWeights(currentBoneWeights);
×
174
            }
×
175

176
            finVertices[scbVertices.Length - 1 - i] = finVertex;
×
177
          }
×
178

179
          var trianglesByMaterial
×
180
              = meshSection.Faces
×
181
                           .ToListDictionary(
×
182
                               f => f.MaterialId,
×
183
                               f => {
×
184
                                 var v0 = finVertices[f.Vertex0];
×
185
                                 var v1 = finVertices[f.Vertex1];
×
186
                                 var v2 = finVertices[f.Vertex2];
×
187
                                 return (v0, v1, v2);
×
188
                               });
×
189

190
          foreach (var materialId in trianglesByMaterial.Keys) {
×
191
            var finPrimitive = finMesh.AddTriangles(
×
192
                (IReadOnlyList<(IReadOnlyVertex, IReadOnlyVertex,
×
193
                    IReadOnlyVertex)>) trianglesByMaterial[materialId]);
×
194
            finPrimitive.SetMaterial(materialById[materialId]);
×
195
            finPrimitive.SetVertexOrder(VertexOrder.COUNTER_CLOCKWISE);
×
196
          }
×
197

198
          break;
×
199
        }
200
      }
201
    }
×
202

203
    return finModel;
×
204
  }
×
205

206
  private class BallAttributes {
207
    public string Id { get; set; }
×
208
    public string? Geometry { get; set; }
×
209
    public string? Texture { get; set; }
×
210
    public bool MapFromFile { get; set; }
×
211
    public bool NoEnvMap { get; set; }
×
212
  }
213

214
  private static Vector2 AdjustVector2_(Vector2 input)
215
    => input with { Y = 1 - input.Y };
×
216

217
  private static Vector3 AdjustVector3_(Vector3 input) => input;
×
218

219
  private static Quaternion ConvertRotationToQuaternion_(Vector3 input)
220
    => input.CreateZyxRadians();
×
221

222
  private static IList<BallAttributes> ReadAllBallAttributes_(
223
      IReadOnlyTreeFile file) {
×
224
    using var tr = new SchemaTextReader(file.OpenRead());
×
225

226
    var allAttributes = new List<BallAttributes>();
×
227

228
    BallAttributes currentAttributes = default!;
×
229
    while (!tr.Eof) {
×
230
      var line = tr.ReadLine().Trim();
×
231

232
      if (line.StartsWith('[')) {
×
233
        var id = line[1..].SubstringUpTo(']');
×
234
        currentAttributes = new BallAttributes { Id = id };
×
235
        allAttributes.Add(currentAttributes);
×
236
      } else {
×
237
        var equalsParts = line.Split('=');
×
238
        if (equalsParts.Length > 1) {
×
239
          var before = equalsParts[0].Trim();
×
240
          var after = equalsParts[1].Trim();
×
241

242
          switch (before) {
×
243
            case "Geometry": {
×
244
              currentAttributes.Geometry = after;
×
245
              break;
×
246
            }
247
            case "MapFromFile": {
×
248
              currentAttributes.MapFromFile = after == "1";
×
249
              break;
×
250
            }
251
            case "NoEnvMap": {
×
252
              currentAttributes.NoEnvMap = after == "1";
×
253
              break;
×
254
            }
255
            case "Texture": {
×
256
              currentAttributes.Texture = after;
×
257
              break;
×
258
            }
259
          }
260
        }
×
261
      }
×
262
    }
×
263

264
    return allAttributes;
×
265
  }
×
266
}
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