mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
vibecoded inspector
This commit is contained in:
@@ -9,6 +9,10 @@ namespace NResUI.Rendering.Viewport.Msh;
|
||||
|
||||
public static class MshRestPoseBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Собирает bind/rest pose для pieces из fallback keyframes 0x08.
|
||||
/// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<MshPieceRestPose> BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, List<Msh0x08.AnimationDescriptor> animationDescriptors)
|
||||
{
|
||||
var nodeList = nodesComponent.Nodes;
|
||||
@@ -54,15 +58,12 @@ public static class MshRestPoseBuilder
|
||||
else
|
||||
{
|
||||
localTransform = Matrix4x4.Identity;
|
||||
|
||||
Console.WriteLine($"Node {nodeIndex} has no fallback");
|
||||
}
|
||||
|
||||
if (parentIndex == -1)
|
||||
{
|
||||
// Root nodes describe object placement in game space; the viewer keeps the model centered.
|
||||
localTransform.Translation = Vector3.Zero;
|
||||
|
||||
Console.WriteLine($"Node {nodeIndex} has no parent");
|
||||
}
|
||||
|
||||
var meshSpaceTransform = localTransform;
|
||||
@@ -86,15 +87,7 @@ public static class MshRestPoseBuilder
|
||||
|
||||
private static int GetParentIndex(Msh0x01.Node node)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Convert.ToInt32(node.ParentIndexOrLink);
|
||||
}
|
||||
catch
|
||||
{
|
||||
var rawParent = Convert.ToUInt16(node.ParentIndexOrLink);
|
||||
return rawParent == ushort.MaxValue ? -1 : rawParent;
|
||||
}
|
||||
return node.ParentIndexOrLink == ushort.MaxValue ? -1 : node.ParentIndexOrLink;
|
||||
}
|
||||
|
||||
private static int GetFallbackKeyframeIndex(Msh0x01.Node node)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace NResUI.Rendering.Viewport.Msh;
|
||||
|
||||
public sealed class MshViewportLoadResult
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public string? Error { get; }
|
||||
public string? SourcePath { get; }
|
||||
public IReadOnlyList<ViewportPiece> Pieces { get; }
|
||||
|
||||
private MshViewportLoadResult(
|
||||
bool isSuccess,
|
||||
string? error,
|
||||
string? sourcePath,
|
||||
IReadOnlyList<ViewportPiece> pieces)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
Error = error;
|
||||
SourcePath = sourcePath;
|
||||
Pieces = pieces;
|
||||
}
|
||||
|
||||
public static MshViewportLoadResult Success(string sourcePath, IReadOnlyList<ViewportPiece> pieces)
|
||||
{
|
||||
return new MshViewportLoadResult(true, null, sourcePath, pieces);
|
||||
}
|
||||
|
||||
public static MshViewportLoadResult Failure(string error, string? sourcePath = null)
|
||||
{
|
||||
return new MshViewportLoadResult(false, error, sourcePath, Array.Empty<ViewportPiece>());
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
using System.Numerics;
|
||||
using MshLib;
|
||||
using NResLib;
|
||||
using NResUI.Abstractions;
|
||||
using NResUI.Rendering.Viewport.Meshes;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace NResUI.Rendering.Viewport.Msh;
|
||||
|
||||
public static class MshViewportLoader
|
||||
{
|
||||
private const int DefaultModelState = 0;
|
||||
private const int DefaultLod = 0;
|
||||
|
||||
public static MshViewportLoadResult LoadFromFile(GL gl, string path, IConfigProvider configProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var parseResult = NResParser.ReadFile(fs);
|
||||
if (parseResult.Archive == null)
|
||||
return MshViewportLoadResult.Failure(parseResult.Error ?? "Failed to parse MSH/NRes file.", path);
|
||||
|
||||
var archive = parseResult.Archive;
|
||||
var meshType = MshConverter.DetectMeshType(archive);
|
||||
if (meshType != MshType.Model)
|
||||
{
|
||||
return MshViewportLoadResult.Failure(
|
||||
$"Only normal MSH models are supported for this viewport pass. Detected: {meshType}.",
|
||||
path);
|
||||
}
|
||||
|
||||
fs.Seek(0, SeekOrigin.Begin);
|
||||
var pieces = LoadModelPieces(gl, fs, archive, path, configProvider);
|
||||
if (pieces.Count == 0)
|
||||
return MshViewportLoadResult.Failure("MSH was parsed, but no renderable geometry pieces were found.", path);
|
||||
|
||||
return MshViewportLoadResult.Success(path, pieces);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MshViewportLoadResult.Failure(ex.Message, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ViewportPiece> LoadModelPieces(
|
||||
GL gl, FileStream fs, NResArchive archive, string path, IConfigProvider configProvider
|
||||
)
|
||||
{
|
||||
var nodes = Msh0x01.ReadComponent(fs, archive);
|
||||
var geometry = Msh0x02.ReadComponent(fs, archive);
|
||||
var positions = Msh0x03.ReadComponent(fs, archive);
|
||||
var uvs = TryReadUvs(fs, archive);
|
||||
var indices = Msh0x06.ReadComponent(fs, archive);
|
||||
var batches = Msh0x0D.ReadComponent(fs, archive);
|
||||
var animationDescriptors = Msh0x08.ReadComponent(fs, archive);
|
||||
var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors);
|
||||
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f);
|
||||
var names = TryReadNames(fs, archive);
|
||||
var materialLibrary = WeaMaterialLibrary.TryLoadForMsh(gl, path, configProvider);
|
||||
|
||||
var pieces = new List<ViewportPiece>();
|
||||
|
||||
for (var nodeIndex = 0; nodeIndex < nodes.Nodes.Count; nodeIndex++)
|
||||
{
|
||||
var node = nodes.Nodes[nodeIndex];
|
||||
var restPose = restPoses[nodeIndex];
|
||||
var slotIndex = node.ResolveSlotIndex(DefaultModelState, DefaultLod);
|
||||
if (slotIndex == ushort.MaxValue)
|
||||
continue;
|
||||
|
||||
if (slotIndex >= geometry.Slots.Count)
|
||||
continue;
|
||||
|
||||
var slot = geometry.Slots[slotIndex];
|
||||
if (!slot.HasBatches)
|
||||
continue;
|
||||
|
||||
var meshBuildResult = BuildPieceMeshes(gl, nodeIndex, slot, positions, uvs, indices, batches, materialLibrary);
|
||||
if (meshBuildResult == null)
|
||||
continue;
|
||||
|
||||
var name = ResolvePieceName(names, nodeIndex);
|
||||
pieces.Add(new ViewportPiece(
|
||||
id: nodeIndex,
|
||||
name: name,
|
||||
meshes: meshBuildResult.Meshes,
|
||||
localTransform: restPose.MeshSpaceTransform * mshToViewportTransform,
|
||||
boundsMin: meshBuildResult.BoundsMin,
|
||||
boundsMax: meshBuildResult.BoundsMax,
|
||||
debugInfo: new ViewportPieceDebugInfo
|
||||
{
|
||||
SourceKind = "MSH 0x01 piece",
|
||||
SourcePieceIndex = nodeIndex,
|
||||
SourceParentIndex = restPose.ParentIndex,
|
||||
GeometrySlotIndex = slotIndex,
|
||||
Msh01Flags = (uint)node.Flags,
|
||||
BatchCount = meshBuildResult.BatchCount,
|
||||
TriangleCount = meshBuildResult.TriangleCount,
|
||||
FallbackKeyframeIndex = restPose.FallbackKeyframeIndex,
|
||||
HasRestPose = restPose.HasFallbackPose
|
||||
}));
|
||||
}
|
||||
|
||||
return pieces;
|
||||
}
|
||||
|
||||
private static PieceMeshBuildResult? BuildPieceMeshes(
|
||||
GL gl,
|
||||
int nodeIndex,
|
||||
Msh0x02.GeometrySlot slot,
|
||||
IReadOnlyList<Common.Vector3> positions,
|
||||
IReadOnlyList<Msh05Uv> uvs,
|
||||
IReadOnlyList<ushort> indices,
|
||||
IReadOnlyList<Msh0x0D.Batch> batches,
|
||||
WeaMaterialLibrary materialLibrary)
|
||||
{
|
||||
var meshes = new List<GpuMesh>();
|
||||
var debugColor = PickDebugColor(nodeIndex);
|
||||
|
||||
var batchCount = 0;
|
||||
var triangleCount = 0;
|
||||
|
||||
var boundsMin = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
var boundsMax = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
|
||||
|
||||
for (var batchIndex = slot.BatchStart0x0D; batchIndex < slot.BatchEndExclusive0x0D; batchIndex++)
|
||||
{
|
||||
if (batchIndex < 0 || batchIndex >= batches.Count)
|
||||
continue;
|
||||
|
||||
var batch = batches[batchIndex];
|
||||
if (batch.IndexStart0x06 + batch.IndexCount0x06 > indices.Count)
|
||||
continue;
|
||||
|
||||
var vertices = new List<float>();
|
||||
var outIndices = new List<uint>();
|
||||
var batchTriangleCount = 0;
|
||||
|
||||
var material = materialLibrary.FindMaterial(batch.MaterialIndexLo) ?? ViewportMaterial.Untextured;
|
||||
var vertexColor = material.HasTexture ? Vector3.One : debugColor;
|
||||
|
||||
for (var i = 0; i + 2 < batch.IndexCount0x06; i += 3)
|
||||
{
|
||||
var indexBase = (int)batch.IndexStart0x06 + i;
|
||||
var vertexIndex0 = checked((int)batch.BaseVertex0x03 + indices[indexBase + 0]);
|
||||
var vertexIndex1 = checked((int)batch.BaseVertex0x03 + indices[indexBase + 1]);
|
||||
var vertexIndex2 = checked((int)batch.BaseVertex0x03 + indices[indexBase + 2]);
|
||||
|
||||
if (!IsValidTriangle(positions.Count, vertexIndex0, vertexIndex1, vertexIndex2))
|
||||
continue;
|
||||
|
||||
var p0 = ToNumericsVector3(positions[vertexIndex0]);
|
||||
var p1 = ToNumericsVector3(positions[vertexIndex1]);
|
||||
var p2 = ToNumericsVector3(positions[vertexIndex2]);
|
||||
|
||||
var normal = Vector3.Cross(p1 - p0, p2 - p0);
|
||||
if (normal.LengthSquared() < 1e-8f)
|
||||
normal = Vector3.UnitY;
|
||||
else
|
||||
normal = Vector3.Normalize(normal);
|
||||
|
||||
AddTriangleVertex(p0, vertexColor, normal, GetUv(uvs, vertexIndex0), vertices, outIndices, ref boundsMin, ref boundsMax);
|
||||
AddTriangleVertex(p1, vertexColor, normal, GetUv(uvs, vertexIndex1), vertices, outIndices, ref boundsMin, ref boundsMax);
|
||||
AddTriangleVertex(p2, vertexColor, normal, GetUv(uvs, vertexIndex2), vertices, outIndices, ref boundsMin, ref boundsMax);
|
||||
batchTriangleCount++;
|
||||
}
|
||||
|
||||
if (batchTriangleCount == 0)
|
||||
continue;
|
||||
|
||||
batchCount++;
|
||||
triangleCount += batchTriangleCount;
|
||||
meshes.Add(PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles, material));
|
||||
}
|
||||
|
||||
if (triangleCount == 0 || meshes.Count == 0)
|
||||
return null;
|
||||
|
||||
return new PieceMeshBuildResult(meshes, boundsMin, boundsMax, batchCount, triangleCount);
|
||||
}
|
||||
|
||||
private static Vector2 GetUv(IReadOnlyList<Msh05Uv> uvs, int vertexIndex)
|
||||
{
|
||||
if (vertexIndex < 0 || vertexIndex >= uvs.Count)
|
||||
return Vector2.Zero;
|
||||
|
||||
var uv = uvs[vertexIndex];
|
||||
return new Vector2(uv.U / 1024.0f, uv.V / 1024.0f);
|
||||
}
|
||||
|
||||
private static Vector3 ToNumericsVector3(Common.Vector3 position)
|
||||
{
|
||||
return new Vector3(position.X, position.Y, position.Z);
|
||||
}
|
||||
|
||||
private static void AddTriangleVertex(
|
||||
Vector3 position,
|
||||
Vector3 color,
|
||||
Vector3 normal,
|
||||
Vector2 uv,
|
||||
List<float> vertices,
|
||||
List<uint> indices,
|
||||
ref Vector3 boundsMin,
|
||||
ref Vector3 boundsMax)
|
||||
{
|
||||
var vertexIndex = (uint)(vertices.Count / PrimitiveMeshes.FloatsPerVertex);
|
||||
|
||||
vertices.Add(position.X);
|
||||
vertices.Add(position.Y);
|
||||
vertices.Add(position.Z);
|
||||
vertices.Add(color.X);
|
||||
vertices.Add(color.Y);
|
||||
vertices.Add(color.Z);
|
||||
vertices.Add(normal.X);
|
||||
vertices.Add(normal.Y);
|
||||
vertices.Add(normal.Z);
|
||||
vertices.Add(uv.X);
|
||||
vertices.Add(uv.Y);
|
||||
|
||||
indices.Add(vertexIndex);
|
||||
|
||||
boundsMin = Vector3.Min(boundsMin, position);
|
||||
boundsMax = Vector3.Max(boundsMax, position);
|
||||
}
|
||||
|
||||
private static bool IsValidTriangle(int vertexCount, int vertexIndex0, int vertexIndex1, int vertexIndex2)
|
||||
{
|
||||
return IsValidVertexIndex(vertexCount, vertexIndex0) &&
|
||||
IsValidVertexIndex(vertexCount, vertexIndex1) &&
|
||||
IsValidVertexIndex(vertexCount, vertexIndex2);
|
||||
}
|
||||
|
||||
private static bool IsValidVertexIndex(int vertexCount, int vertexIndex)
|
||||
{
|
||||
return vertexIndex >= 0 && vertexIndex < vertexCount;
|
||||
}
|
||||
|
||||
private static List<string> TryReadNames(FileStream fs, NResArchive archive)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Msh0x0A.ReadComponent(fs, archive);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Msh05Uv> TryReadUvs(FileStream fs, NResArchive archive)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Msh0x05.ReadComponent(fs, archive);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Msh05Uv>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolvePieceName(IReadOnlyList<string> names, int nodeIndex)
|
||||
{
|
||||
if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex]))
|
||||
return $"{nodeIndex}: {names[nodeIndex]}";
|
||||
|
||||
return $"{nodeIndex}: piece_{nodeIndex:D3}";
|
||||
}
|
||||
|
||||
private static Vector3 PickDebugColor(int index)
|
||||
{
|
||||
ReadOnlySpan<Vector3> palette =
|
||||
[
|
||||
new Vector3(0.90f, 0.36f, 0.30f),
|
||||
new Vector3(0.30f, 0.68f, 0.95f),
|
||||
new Vector3(0.42f, 0.82f, 0.42f),
|
||||
new Vector3(0.95f, 0.78f, 0.32f),
|
||||
new Vector3(0.78f, 0.48f, 0.95f),
|
||||
new Vector3(0.36f, 0.86f, 0.78f),
|
||||
new Vector3(0.95f, 0.55f, 0.78f),
|
||||
new Vector3(0.72f, 0.72f, 0.72f),
|
||||
];
|
||||
|
||||
return palette[index % palette.Length];
|
||||
}
|
||||
|
||||
private sealed record PieceMeshBuildResult(
|
||||
IReadOnlyList<GpuMesh> Meshes,
|
||||
Vector3 BoundsMin,
|
||||
Vector3 BoundsMax,
|
||||
int BatchCount,
|
||||
int TriangleCount);
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
using MaterialLib;
|
||||
using NResLib;
|
||||
using NResUI.Abstractions;
|
||||
using NResUI.Rendering.Viewport.OpenGL;
|
||||
using Silk.NET.OpenGL;
|
||||
using TexmLib;
|
||||
|
||||
namespace NResUI.Rendering.Viewport.Msh;
|
||||
|
||||
public sealed class WeaMaterialLibrary
|
||||
{
|
||||
private readonly Dictionary<int, ViewportMaterial> _materialsById;
|
||||
|
||||
private WeaMaterialLibrary(Dictionary<int, ViewportMaterial> materialsById)
|
||||
{
|
||||
_materialsById = materialsById;
|
||||
}
|
||||
|
||||
public static WeaMaterialLibrary Empty { get; } = new(new Dictionary<int, ViewportMaterial>());
|
||||
|
||||
public ViewportMaterial? FindMaterial(int id)
|
||||
{
|
||||
return _materialsById.TryGetValue(id, out var material)
|
||||
? material
|
||||
: null;
|
||||
}
|
||||
|
||||
public static WeaMaterialLibrary TryLoadForMsh(GL gl, string mshPath, IConfigProvider configProvider)
|
||||
{
|
||||
var weaPath = FindMatchingWeaPath(mshPath);
|
||||
if (!File.Exists(weaPath))
|
||||
return Empty;
|
||||
|
||||
var materialRefs = ParseMaterialRefs(weaPath);
|
||||
if (materialRefs.Count == 0)
|
||||
return Empty;
|
||||
|
||||
var materialLibFs = Path.Combine(configProvider.GetConfig().GameBasePath, "Material.lib");
|
||||
|
||||
if (!File.Exists(materialLibFs))
|
||||
{
|
||||
return new WeaMaterialLibrary([]);
|
||||
}
|
||||
|
||||
using var materialFs = new FileStream(materialLibFs, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var parseResult = NResParser.ReadFile(materialFs);
|
||||
if (parseResult.Archive == null)
|
||||
return Empty;
|
||||
|
||||
var result = new Dictionary<int, ViewportMaterial>();
|
||||
|
||||
foreach (var materialRef in materialRefs)
|
||||
{
|
||||
var texture = TryLoadMaterialTexture(gl, materialFs, parseResult.Archive, materialRef.Name, configProvider, out var texm);
|
||||
result[materialRef.Id] = texture != null
|
||||
? new ViewportMaterial(materialRef.Name, texture.Value, texm)
|
||||
: new ViewportMaterial(materialRef.Name);
|
||||
}
|
||||
|
||||
return new WeaMaterialLibrary(result);
|
||||
}
|
||||
|
||||
private static string? FindMatchingWeaPath(string mshPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(mshPath);
|
||||
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory))
|
||||
return null;
|
||||
|
||||
var mshResourceName = GetExportedResourceNameWithoutExtension(mshPath);
|
||||
var mshObjectStem = ExtractObjectStem(mshResourceName);
|
||||
|
||||
var candidates = Directory.EnumerateFiles(directory, "*.wea", SearchOption.TopDirectoryOnly)
|
||||
.Select(path => new
|
||||
{
|
||||
Path = path,
|
||||
ResourceName = GetExportedResourceNameWithoutExtension(path),
|
||||
})
|
||||
.Select(x => new
|
||||
{
|
||||
x.Path,
|
||||
x.ResourceName,
|
||||
ObjectStem = ExtractObjectStem(x.ResourceName),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Best case: exact resource stem match after type prefix normalization.
|
||||
var exact = candidates.FirstOrDefault(x => string.Equals(x.ObjectStem, mshObjectStem,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (exact != null)
|
||||
return exact.Path;
|
||||
|
||||
// Fallback: choose a WEA whose resource name shares the longest token prefix.
|
||||
var best = candidates
|
||||
.Select(x => new
|
||||
{
|
||||
x.Path,
|
||||
Score = CommonPrefixTokenCount(
|
||||
SplitResourceTokens(mshObjectStem),
|
||||
SplitResourceTokens(x.ObjectStem))
|
||||
})
|
||||
.OrderByDescending(x => x.Score)
|
||||
.FirstOrDefault();
|
||||
|
||||
return best?.Score > 0 ? best.Path : null;
|
||||
}
|
||||
|
||||
private static string GetExportedResourceNameWithoutExtension(string path)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
// Exported files are usually like:
|
||||
// 58_MESH_o_tur_ba_06
|
||||
// 81_WEAR_o_tur_ba_02
|
||||
//
|
||||
// Strip numeric export prefix.
|
||||
var firstUnderscore = name.IndexOf('_');
|
||||
if (firstUnderscore > 0 &&
|
||||
name[..firstUnderscore].All(char.IsDigit))
|
||||
{
|
||||
name = name[(firstUnderscore + 1)..];
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string ExtractObjectStem(string resourceName)
|
||||
{
|
||||
// Normalize known resource kind prefixes.
|
||||
// MESH_o_tur_ba_06 -> o_tur_ba
|
||||
// WEAR_o_tur_ba_02 -> o_tur_ba
|
||||
var normalized = resourceName;
|
||||
|
||||
foreach (var prefix in new[] { "MESH_", "WEAR_", "TEXT_", "ANIM_" })
|
||||
{
|
||||
if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized = normalized[prefix.Length..];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var tokens = normalized.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return string.Join('_', tokens);
|
||||
}
|
||||
|
||||
private static string[] SplitResourceTokens(string value)
|
||||
{
|
||||
return value.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
private static int CommonPrefixTokenCount(string[] a, string[] b)
|
||||
{
|
||||
var count = Math.Min(a.Length, b.Length);
|
||||
var result = 0;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (!string.Equals(a[i], b[i], StringComparison.OrdinalIgnoreCase))
|
||||
break;
|
||||
|
||||
result++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<WeaMaterialRef> ParseMaterialRefs(string weaPath)
|
||||
{
|
||||
var lines = File.ReadAllLines(weaPath)
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => x.Length != 0)
|
||||
.ToList();
|
||||
|
||||
if (lines.Count == 0 || !int.TryParse(lines[0], out var materialCount) || materialCount <= 0)
|
||||
return [];
|
||||
|
||||
var result = new List<WeaMaterialRef>();
|
||||
|
||||
for (var i = 0; i < materialCount && i + 1 < lines.Count; i++)
|
||||
{
|
||||
var line = lines[i + 1];
|
||||
if (line.Equals("LIGHTMAPS", StringComparison.OrdinalIgnoreCase))
|
||||
break;
|
||||
|
||||
var parts = line.Split((char[]?)null, 2,
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length < 2)
|
||||
continue;
|
||||
|
||||
if (!int.TryParse(parts[0], out var id))
|
||||
continue;
|
||||
|
||||
result.Add(new WeaMaterialRef(id, parts[1]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static uint? TryLoadMaterialTexture(
|
||||
GL gl, FileStream materialFs, NResArchive materialArchive, string materialName, IConfigProvider configProvider,
|
||||
out TexmFile? texm
|
||||
)
|
||||
{
|
||||
texm = null;
|
||||
var entry = FindMaterialEntry(materialArchive, materialName);
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
materialFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
var materialData = new byte[entry.FileLength];
|
||||
materialFs.ReadExactly(materialData, 0, materialData.Length);
|
||||
|
||||
using var ms = new MemoryStream(materialData, writable: false);
|
||||
|
||||
var materialFile = MaterialParser.ReadFromStream(ms, materialName, entry.ElementCount, entry.Magic1);
|
||||
|
||||
var texture = materialFile.Stages[0].TextureName;
|
||||
|
||||
var textureLibFs = Path.Combine(configProvider.GetConfig().GameBasePath, "Textures.lib");
|
||||
|
||||
if (!File.Exists(textureLibFs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var textureFs = new FileStream(textureLibFs, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var textureResult = NResParser.ReadFile(textureFs);
|
||||
if (textureResult.Archive == null)
|
||||
return 0;
|
||||
|
||||
var textureEntry = FindTextureEntry(textureResult.Archive, texture);
|
||||
if (textureEntry is null)
|
||||
{
|
||||
Console.WriteLine($"Didnt find texture {texture}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
textureFs.Seek(textureEntry.OffsetInFile, SeekOrigin.Begin);
|
||||
var textureData = new byte[textureEntry.FileLength];
|
||||
textureFs.ReadExactly(textureData, 0, textureData.Length);
|
||||
|
||||
using var textureMs = new MemoryStream(textureData, writable: false);
|
||||
|
||||
var texmResult = TexmParser.ReadFromStream(textureMs, entry.FileName);
|
||||
if (texmResult.TexmFile == null)
|
||||
return null;
|
||||
|
||||
texm = texmResult.TexmFile;
|
||||
var rgba = texmResult.TexmFile.GetRgba32BytesFromMipmap(0, out var width, out var height);
|
||||
return ViewportTextureLoader.CreateRgbaTexture(gl, rgba, width, height);
|
||||
}
|
||||
|
||||
private static ListMetadataItem? FindMaterialEntry(NResArchive materialArchive, string materialName)
|
||||
{
|
||||
static string Normalize(string value)
|
||||
{
|
||||
return value.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
var normalized = Normalize(materialName);
|
||||
|
||||
return materialArchive.Files.FirstOrDefault(x =>
|
||||
string.Equals(x.FileName, materialName, StringComparison.OrdinalIgnoreCase) ||
|
||||
Normalize(x.FileName) == normalized);
|
||||
}
|
||||
|
||||
private static ListMetadataItem? FindTextureEntry(NResArchive archive, string name)
|
||||
{
|
||||
static string Normalize(string value)
|
||||
{
|
||||
return value.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
var normalized = Normalize(name);
|
||||
|
||||
return archive.Files.FirstOrDefault(x =>
|
||||
string.Equals(x.FileName, name, StringComparison.OrdinalIgnoreCase) ||
|
||||
Normalize(x.FileName) == normalized);
|
||||
}
|
||||
|
||||
private readonly record struct WeaMaterialRef(int Id, string Name);
|
||||
}
|
||||
Reference in New Issue
Block a user