restore monogltf

This commit is contained in:
tresabhi committed 2026-06-06 13:22:54 -05:00
1 parent 4b0035345e
commit 39ffc6ae51
5 files changed
+394 -332

No files matched your search

+2 -1
View File
@@ -8,7 +8,8 @@
"blitzkit", "blitzkit",
"webp", "webp",
"hocs", "hocs",
"cooldown" "cooldown",
"Lods"
], ],
"userWords": [ "userWords": [
"WOTB", "WOTB",
+13
View File
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.6",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
+136 -111
View File
@@ -1,150 +1,175 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using BlitzKit.Game.Models; using BlitzKit.Game.Models;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion.Textures; using CUE4Parse_Conversion.Textures;
using CUE4Parse.Compression; using CUE4Parse_Conversion.UEFormat.Enums;
using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse.UE4.Assets.Exports.Texture; using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse.UE4.Assets.Objects.Properties; using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.IO.Objects;
using CUE4Parse.UE4.Objects.Engine; using CUE4Parse.UE4.Objects.Engine;
using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Objects.UObject;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.JavaScript.NodeApi; using Microsoft.JavaScript.NodeApi;
using SkiaSharp; using SkiaSharp;
using CUE4Parse.UE4.AssetRegistry.Objects;
using CUE4Parse.UE4.Readers;
using CUE4Parse.UE4.AssetRegistry.Readers;
using CUE4Parse.UE4.AssetRegistry;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
using CUE4Parse_Conversion.Meshes;
using CUE4Parse_Conversion;
namespace BlitzKit.Game.JSExport; namespace BlitzKit.Game.JSExport;
[JSExport] [JSExport]
public partial class GameInterface public partial class GameInterface
{ {
private readonly BlitzFileProvider provider; private readonly BlitzFileProvider provider;
private readonly BlitzExporter exporter;
public HashSet<string> files; public HashSet<string> files;
public GameInterface(string directory, string map, string temp) public GameInterface(string directory, string map, string temp)
{
provider = new(directory, map, temp);
files = [.. provider.Files.Keys];
}
public HashSet<string> Files => files;
public byte[] TankBigIcon(string tag)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("BigIcon");
return provider.Image(icon);
}
[GeneratedRegex(@"T_UI_Flag_(\w+)_S")]
private static partial Regex FlagNameRegex();
public byte[] Flag(string nation)
{
var flagsBase = "Blitz/Content/UI/Textures/Flag/TankCard/";
foreach (var path in provider.Files.Keys)
{ {
if (!path.StartsWith(flagsBase)) provider = new(directory, map, temp);
{ exporter = new();
continue;
}
var fileName = Path.GetFileNameWithoutExtension(path); files = [.. provider.Files.Keys];
var match = FlagNameRegex().Match(fileName); }
if ( public HashSet<string> Files => files;
match.Success
&& match.Groups[1].Value.Equals(nation, StringComparison.CurrentCultureIgnoreCase) public byte[] TankBigIcon(string tag)
) {
{ var pda = provider.Discovered<UPrimaryDataAsset>(tag);
if (provider.TryLoadPackageObject<UTexture2D>($"{path}.{fileName}", out var uTexture)) var icon = pda.Get<FSoftObjectPath>("BigIcon");
return provider.Image(icon);
}
[GeneratedRegex(@"T_UI_Flag_(\w+)_S")]
private static partial Regex FlagNameRegex();
public byte[] Flag(string nation)
{
var flagsBase = "Blitz/Content/UI/Textures/Flag/TankCard/";
foreach (var path in provider.Files.Keys)
{ {
CTexture? cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile); if (!path.StartsWith(flagsBase))
{
continue;
}
if (cTexture is null) var fileName = Path.GetFileNameWithoutExtension(path);
{ var match = FlagNameRegex().Match(fileName);
Console.WriteLine($"No flag found for {nation}");
return [];
}
SKBitmap bitmap = cTexture.ToSkBitmap(); if (
SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, 80); match.Success
&& match.Groups[1].Value.Equals(nation, StringComparison.CurrentCultureIgnoreCase)
)
{
if (
provider.TryLoadPackageObject<UTexture2D>(
$"{path}.{fileName}",
out var uTexture
)
)
{
CTexture? cTexture = uTexture.Decode(ETexturePlatform.DesktopMobile);
return data.ToArray(); if (cTexture is null)
{
Console.WriteLine($"No flag found for {nation}");
return [];
}
SKBitmap bitmap = cTexture.ToSkBitmap();
SKData data = bitmap.Encode(SKEncodedImageFormat.Webp, 80);
return data.ToArray();
}
Console.WriteLine($"No flag found for {nation}");
return [];
}
} }
Console.WriteLine($"No flag found for {nation}"); Console.WriteLine($"No flag found for {nation}");
return []; return [];
}
} }
Console.WriteLine($"No flag found for {nation}"); public byte[] ConsumableIcon(string tag)
return [];
}
public byte[] ConsumableIcon(string tag)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
return provider.Image(icon);
}
public byte[] EquipmentIcon(string tag)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
return provider.Image(icon);
}
readonly Dictionary<string, string> tankPartPrefixes = new()
{
{"chassis", "DT_Chassis"},
{"hull", "DT_Hulls"},
{"turret", "DT_Turrets"},
{"gun", "DT_Guns"},
};
public byte[] TankPart(string tag, string part)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
UDataTable? dataTable = null;
foreach (var (prefix, name) in tankPartPrefixes)
{ {
if (!part.StartsWith(prefix)) { continue; } var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
dataTable = pda.Get<UDataTable>(name); return provider.Image(icon);
break;
} }
if (dataTable == null) public byte[] EquipmentIcon(string tag)
{ {
throw new ArgumentException($"Unknown tank part: {part}"); var pda = provider.Discovered<UPrimaryDataAsset>(tag);
var icon = pda.Get<FSoftObjectPath>("Icon");
return provider.Image(icon);
} }
dataTable.TryGetDataTableRow(part, StringComparison.Ordinal, out var row); readonly Dictionary<string, string> tankPartPrefixes = new()
if (row == null)
{ {
throw new ArgumentException($"Unknown tank part: {part}"); { "chassis", "DT_Chassis" },
{ "hull", "DT_Hulls" },
{ "turret", "DT_Turrets" },
{ "gun", "DT_Guns" },
};
public byte[] TankPart(string tag, string part)
{
var pda = provider.Discovered<UPrimaryDataAsset>(tag);
UDataTable? dataTable = null;
foreach (var (prefix, name) in tankPartPrefixes)
{
if (!part.StartsWith(prefix))
{
continue;
}
dataTable = pda.Get<UDataTable>(name);
break;
}
if (dataTable == null)
{
throw new ArgumentException($"Unknown tank part: {part}");
}
dataTable.TryGetDataTableRow(part, StringComparison.Ordinal, out var row);
if (row == null)
{
throw new ArgumentException($"Unknown tank part: {part}");
}
var visualData = row.Get<UObject>("VisualData");
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
// var collisionMesh = meshSettings.Get<UStaticMesh>("CollisionMesh");
var mesh = meshSettings.Get<UStaticMesh>("GunMesh");
var gltf = new MonoGltf(mesh);
exporter.glb(mesh);
// var options = new ExporterOptions() {
// LodFormat = ELodFormat.FirstLod,
// MeshFormat = EMeshFormat.Gltf2,
// TextureFormat = ETextureFormat.Jpeg,
// };
// var exporter = new MeshExporter(mesh, options);
// var lod0 = exporter.MeshLods[0];
// exporter.TryWriteToDir(new("../../temp/model"), out var _, out var _);
// Console.WriteLine(lod0.FileName);
// Console.WriteLine(lod0.FileData.Length);
// foreach (var material in lod0.Materials)
// {
// material.ma
// // material.TryWriteToDir
// }
return [];
} }
var visualData = row.Get<UObject>("VisualData");
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
// var collisionMesh = meshSettings.Get<UStaticMesh>("CollisionMesh");
var mesh = meshSettings.Get<UStaticMesh>("GunMesh");
var gltf = new MonoGltf(mesh);
return [.. gltf.Write()];
}
} }
+241 -219
View File
@@ -18,247 +18,269 @@ using SharpGLTF.Scenes;
namespace BlitzKit.Game.Models; namespace BlitzKit.Game.Models;
using ConfiguredMeshBuilder = MeshBuilder< using ConfiguredMeshBuilder = MeshBuilder<
VertexPositionNormalTangent, VertexPositionNormalTangent,
VertexColorXTextureX, VertexColorXTextureX,
VertexEmpty VertexEmpty
>; >;
public class MonoGltf public class MonoGltf
{ {
readonly SceneBuilder Scene; readonly SceneBuilder Scene;
public MonoGltf(UStaticMesh staticMesh) public MonoGltf(UStaticMesh staticMesh)
{
Scene = new(staticMesh.Name);
ExporterOptions options = new() { TextureFormat = ETextureFormat.Jpeg };
staticMesh.TryConvert(out var convertedMesh);
var lod =
convertedMesh.LODs.Find(lod => !lod.SkipLod) ?? throw new Exception("Failed to find lod0"); // first non-skippable lod is the highest quality
MaterialBuilder emptyMaterial = new("empty_material");
Dictionary<string, MaterialBuilder> namedMaterials = [];
int sectionIndex = 0;
foreach (var section in lod.Sections.Value)
{ {
ConfiguredMeshBuilder meshBuilder = new($"{staticMesh.Name}_section_{sectionIndex}"); Scene = new(staticMesh.Name);
MaterialBuilder materialBuilder; ExporterOptions options = new() { TextureFormat = ETextureFormat.Jpeg };
staticMesh.TryConvert(out var convertedMesh);
var lod =
convertedMesh.LODs.Find(lod => !lod.SkipLod)
?? throw new Exception("Failed to find lod0");
if (section.Material == null || section.MaterialName == null) MaterialBuilder emptyMaterial = new("empty_material");
{ Dictionary<string, MaterialBuilder> namedMaterials = [];
materialBuilder = emptyMaterial;
}
else if (namedMaterials.TryGetValue(section.MaterialName, out MaterialBuilder? value))
{
materialBuilder = value;
}
else
{
var materialInterface = section.Material.Load<UMaterialInterface>()!;
MaterialData materialData = new() { Textures = [], Parameters = new() };
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
materialBuilder = new(section.MaterialName);
foreach (var parameterTexture in materialData.Parameters.Textures) int sectionIndex = 0;
foreach (var section in lod.Sections.Value)
{ {
var texture = parameterTexture.Value; ConfiguredMeshBuilder meshBuilder = new($"{staticMesh.Name}_section_{sectionIndex}");
MaterialBuilder materialBuilder;
if ( if (section.Material == null || section.MaterialName == null)
texture is not UTexture2D texture2d
|| texture2d.Decode(options.Platform) is not { } bitmap
)
return;
try
{
switch (parameterTexture.Key)
{ {
case "BaseColor": materialBuilder = emptyMaterial;
{ }
var encoded = bitmap.Encode(options.TextureFormat, 100); else if (namedMaterials.TryGetValue(section.MaterialName, out MaterialBuilder? value))
var bytes = encoded.ToArray(); {
materialBuilder.WithBaseColor(bytes); materialBuilder = value;
}
else
{
var materialInterface = section.Material.Load<UMaterialInterface>()!;
MaterialData materialData = new() { Textures = [], Parameters = new() };
materialInterface.GetParams(materialData.Parameters, options.MaterialFormat);
materialBuilder = new(section.MaterialName);
break; foreach (var parameterTexture in materialData.Parameters.Textures)
}
case "RM":
{
var encoded = bitmap.Encode(options.TextureFormat, 100);
for (int y = 0; y < bitmap.Height; y++)
{ {
for (int x = 0; x < bitmap.Width; x++) var texture = parameterTexture.Value;
{
var index = y * bitmap.Width + x; if (
var color = bitmap.Pixels[index]; texture is not UTexture2D texture2d
bitmap.Pixels[index] = new(color.Blue, color.Red, color.Green, color.Alpha); || texture2d.Decode(options.Platform) is not { } bitmap
} )
return;
try
{
switch (parameterTexture.Key)
{
case "BaseColor":
{
var encoded = bitmap.Encode(
options.TextureFormat,
false,
out var _
);
var bytes = encoded.ToArray();
materialBuilder.WithBaseColor(bytes);
break;
}
case "RM":
{
var encoded = bitmap.Encode(
options.TextureFormat,
false,
out var _
);
// for (int y = 0; y < bitmap.Height; y++)
// {
// for (int x = 0; x < bitmap.Width; x++)
// {
// var index = y * bitmap.Width + x;
// var color = bitmap.Pixels[index];
// bitmap.Pixels[index] = new(
// color.Blue,
// color.Red,
// color.Green,
// color.Alpha
// );
// }
// }
var bytes = encoded.ToArray();
materialBuilder.WithBaseColor(bytes);
break;
}
case "PM_SpecularMasks":
// materialBuilder.WithSpecularGlossiness(textureBytes);
break;
case "PM_Diffuse":
// materialBuilder.WithDiffuse(textureBytes);
break;
case "T_R90_IS_4_MISC":
case "T_R90_IS_4_MASK":
case "Misc":
case "Mask":
break;
default:
throw new Exception(
$"Unsupported texture type: {parameterTexture.Key}"
);
}
}
catch
{
Console.WriteLine($"Failed to load texture: {parameterTexture.Key}");
}
}
}
var primitive = meshBuilder.UsePrimitive(materialBuilder);
for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++)
{
var wedgeIndex = new int[3];
for (var k = 0; k < wedgeIndex.Length; k++)
{
wedgeIndex[k] = (int)lod.Indices.Value[section.FirstIndex + faceIndex * 3 + k];
} }
var bytes = encoded.ToArray(); var vertex1 = lod.Verts[wedgeIndex[0]];
materialBuilder.WithBaseColor(bytes); var vertex2 = lod.Verts[wedgeIndex[1]];
var vertex3 = lod.Verts[wedgeIndex[2]];
break; var (v1, v2, v3) = PrepareTris(vertex1, vertex2, vertex3);
} var (c1, c2, c3) = PrepareUVsAndTexCoords(
lod,
vertex1,
vertex2,
vertex3,
wedgeIndex
);
case "PM_SpecularMasks": primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3));
// materialBuilder.WithSpecularGlossiness(textureBytes);
break;
case "PM_Diffuse":
// materialBuilder.WithDiffuse(textureBytes);
break;
case "T_R90_IS_4_MISC":
case "T_R90_IS_4_MASK":
case "Misc":
case "Mask":
break;
default:
throw new Exception($"Unsupported texture type: {parameterTexture.Key}");
} }
}
catch Scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
{
PrettyLog.Warn($"Failed to load texture: {parameterTexture.Key}");
}
} }
}
var primitive = meshBuilder.UsePrimitive(materialBuilder);
for (int faceIndex = 0; faceIndex < section.NumFaces; faceIndex++)
{
var wedgeIndex = new int[3];
for (var k = 0; k < wedgeIndex.Length; k++)
{
wedgeIndex[k] = lod.Indices.Value[section.FirstIndex + faceIndex * 3 + k];
}
var vertex1 = lod.Verts[wedgeIndex[0]];
var vertex2 = lod.Verts[wedgeIndex[1]];
var vertex3 = lod.Verts[wedgeIndex[2]];
var (v1, v2, v3) = PrepareTris(vertex1, vertex2, vertex3);
var (c1, c2, c3) = PrepareUVsAndTexCoords(lod, vertex1, vertex2, vertex3, wedgeIndex);
primitive.AddTriangle((v1, c1), (v2, c2), (v3, c3));
}
Scene.AddRigidMesh(meshBuilder, Matrix4x4.Identity);
} }
}
public ArraySegment<byte> Write() public ArraySegment<byte> Write()
{
return Scene.ToGltf2().WriteGLB();
}
private static (
VertexPositionNormalTangent,
VertexPositionNormalTangent,
VertexPositionNormalTangent
) PrepareTris(CMeshVertex vert1, CMeshVertex vert2, CMeshVertex vert3)
{
VertexPositionNormalTangent v1 = new(
SwapYZ(vert1.Position * 0.01f),
SwapYZAndNormalize((FVector)vert1.Normal),
SwapYZAndNormalize((Vector4)vert1.Tangent)
);
VertexPositionNormalTangent v2 = new(
SwapYZ(vert2.Position * 0.01f),
SwapYZAndNormalize((FVector)vert2.Normal),
SwapYZAndNormalize((Vector4)vert2.Tangent)
);
VertexPositionNormalTangent v3 = new(
SwapYZ(vert3.Position * 0.01f),
SwapYZAndNormalize((FVector)vert3.Normal),
SwapYZAndNormalize((Vector4)vert3.Tangent)
);
return (v1, v2, v3);
}
public static FVector SwapYZ(FVector vec)
{
var res = new FVector(vec.X, vec.Z, vec.Y);
return res;
}
public static FVector SwapYZAndNormalize(FVector vec)
{
var res = SwapYZ(vec);
res.Normalize();
return res;
}
public static Vector4 SwapYZAndNormalize(Vector4 vec)
{
return Vector4.Normalize(new Vector4(vec.X, vec.Z, vec.Y, vec.W));
}
public static (
VertexColorXTextureX,
VertexColorXTextureX,
VertexColorXTextureX
) PrepareUVsAndTexCoords(
CBaseMeshLod lod,
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
int[] indices
)
{
return PrepareUVsAndTexCoords(
lod.VertexColors ?? new FColor[lod.NumVerts],
vert1,
vert2,
vert3,
lod.ExtraUV.Value,
indices
);
}
public static (
VertexColorXTextureX,
VertexColorXTextureX,
VertexColorXTextureX
) PrepareUVsAndTexCoords(
FColor[] colors,
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
FMeshUVFloat[][] uvs,
int[] indices
)
{
var (uvs1, uvs2, uvs3) = PrepareUVs(vert1, vert2, vert3, uvs, indices);
var c1 = new VertexColorXTextureX((Vector4)colors[indices[0]] / 255, uvs1);
var c2 = new VertexColorXTextureX((Vector4)colors[indices[1]] / 255, uvs2);
var c3 = new VertexColorXTextureX((Vector4)colors[indices[2]] / 255, uvs3);
return (c1, c2, c3);
}
private static (List<Vector2>, List<Vector2>, List<Vector2>) PrepareUVs(
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
FMeshUVFloat[][] uvs,
int[] indices
)
{
var uvs1 = new List<Vector2>() { (Vector2)vert1.UV };
var uvs2 = new List<Vector2>() { (Vector2)vert2.UV };
var uvs3 = new List<Vector2>() { (Vector2)vert3.UV };
foreach (var uv in uvs)
{ {
uvs1.Add((Vector2)uv[indices[0]]); return Scene.ToGltf2().WriteGLB();
uvs2.Add((Vector2)uv[indices[1]]);
uvs3.Add((Vector2)uv[indices[2]]);
} }
return (uvs1, uvs2, uvs3); private static (
} VertexPositionNormalTangent,
VertexPositionNormalTangent,
VertexPositionNormalTangent
) PrepareTris(CMeshVertex vert1, CMeshVertex vert2, CMeshVertex vert3)
{
VertexPositionNormalTangent v1 = new(
SwapYZ(vert1.Position * 0.01f),
SwapYZAndNormalize((FVector)vert1.Normal),
SwapYZAndNormalize((Vector4)vert1.Tangent)
);
VertexPositionNormalTangent v2 = new(
SwapYZ(vert2.Position * 0.01f),
SwapYZAndNormalize((FVector)vert2.Normal),
SwapYZAndNormalize((Vector4)vert2.Tangent)
);
VertexPositionNormalTangent v3 = new(
SwapYZ(vert3.Position * 0.01f),
SwapYZAndNormalize((FVector)vert3.Normal),
SwapYZAndNormalize((Vector4)vert3.Tangent)
);
return (v1, v2, v3);
}
public static FVector SwapYZ(FVector vec)
{
var res = new FVector(vec.X, vec.Z, vec.Y);
return res;
}
public static FVector SwapYZAndNormalize(FVector vec)
{
var res = SwapYZ(vec);
res.Normalize();
return res;
}
public static Vector4 SwapYZAndNormalize(Vector4 vec)
{
return Vector4.Normalize(new Vector4(vec.X, vec.Z, vec.Y, vec.W));
}
public static (
VertexColorXTextureX,
VertexColorXTextureX,
VertexColorXTextureX
) PrepareUVsAndTexCoords(
CBaseMeshLod lod,
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
int[] indices
)
{
return PrepareUVsAndTexCoords(
lod.VertexColors ?? new FColor[lod.NumVerts],
vert1,
vert2,
vert3,
lod.ExtraUV.Value,
indices
);
}
public static (
VertexColorXTextureX,
VertexColorXTextureX,
VertexColorXTextureX
) PrepareUVsAndTexCoords(
FColor[] colors,
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
FMeshUVFloat[][] uvs,
int[] indices
)
{
var (uvs1, uvs2, uvs3) = PrepareUVs(vert1, vert2, vert3, uvs, indices);
var c1 = new VertexColorXTextureX((Vector4)colors[indices[0]] / 255, uvs1);
var c2 = new VertexColorXTextureX((Vector4)colors[indices[1]] / 255, uvs2);
var c3 = new VertexColorXTextureX((Vector4)colors[indices[2]] / 255, uvs3);
return (c1, c2, c3);
}
private static (List<Vector2>, List<Vector2>, List<Vector2>) PrepareUVs(
CMeshVertex vert1,
CMeshVertex vert2,
CMeshVertex vert3,
FMeshUVFloat[][] uvs,
int[] indices
)
{
var uvs1 = new List<Vector2>() { (Vector2)vert1.UV };
var uvs2 = new List<Vector2>() { (Vector2)vert2.UV };
var uvs3 = new List<Vector2>() { (Vector2)vert3.UV };
foreach (var uv in uvs)
{
uvs1.Add((Vector2)uv[indices[0]]);
uvs2.Add((Vector2)uv[indices[1]]);
uvs3.Add((Vector2)uv[indices[2]]);
}
return (uvs1, uvs2, uvs3);
}
} }
@@ -36,6 +36,7 @@ export async function GET({
const assetDiscovery = item.BlitzStaticAssetsDiscovery(); const assetDiscovery = item.BlitzStaticAssetsDiscovery();
const tag = assertDiscoveryTag(assetDiscovery); const tag = assertDiscoveryTag(assetDiscovery);
const part = game!.tankPart(tag, params.part); const part = game!.tankPart(tag, params.part);
const array = new Uint8Array(part);
return Response.json(part.length); return new Response(array);
} }