mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
148 lines
5.1 KiB
C#
148 lines
5.1 KiB
C#
using WeaLib;
|
||
|
||
namespace NResUI.Rendering.Materials;
|
||
|
||
/// <summary>Результат эвристического поиска WEA рядом с MSH.</summary>
|
||
public sealed class WeaResourceMatch
|
||
{
|
||
public required string Path { get; init; }
|
||
public required string Reason { get; init; }
|
||
public required WeaFile File { get; init; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// UI-side resolver для связи MSH-файла с WEA-файлом.
|
||
/// Это не часть WeaLib, потому что matching зависит от экспортированных имен и расположения ресурсов на диске.
|
||
/// </summary>
|
||
public static class WeaResourceResolver
|
||
{
|
||
/// <summary>
|
||
/// Ищет WEA рядом с MSH и сразу парсит его.
|
||
/// Если файл не найден или битый, viewport все равно может показать модель без материалов.
|
||
/// </summary>
|
||
public static WeaResourceMatch? TryLoadMatchingWeaForMsh(string mshPath, ICollection<string>? warnings = null)
|
||
{
|
||
var match = FindMatchingWeaPath(mshPath, out var reason);
|
||
if (match == null)
|
||
return null;
|
||
|
||
var parseResult = WeaParser.ReadFile(match);
|
||
if (parseResult.WeaFile == null)
|
||
{
|
||
warnings?.Add($"Failed to parse WEA '{match}': {parseResult.Error}");
|
||
return null;
|
||
}
|
||
|
||
return new WeaResourceMatch
|
||
{
|
||
Path = match,
|
||
Reason = reason,
|
||
File = parseResult.WeaFile
|
||
};
|
||
}
|
||
|
||
private static string? FindMatchingWeaPath(string mshPath, out string reason)
|
||
{
|
||
reason = "No matching WEA found.";
|
||
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();
|
||
|
||
var exact = candidates.FirstOrDefault(x => string.Equals(x.ObjectStem, mshObjectStem,
|
||
StringComparison.OrdinalIgnoreCase));
|
||
|
||
// Лучший случай: MESH_* и WEAR_* после нормализации указывают на один объект.
|
||
if (exact != null)
|
||
{
|
||
reason = "Object stem matched exactly.";
|
||
return exact.Path;
|
||
}
|
||
|
||
var best = candidates
|
||
.Select(x => new
|
||
{
|
||
x.Path,
|
||
Score = CommonPrefixTokenCount(
|
||
SplitResourceTokens(mshObjectStem),
|
||
SplitResourceTokens(x.ObjectStem))
|
||
})
|
||
.OrderByDescending(x => x.Score)
|
||
.FirstOrDefault();
|
||
|
||
// Fallback нужен для экспортов, где суффиксы MESH/WEAR отличаются, но начало имени объекта совпадает.
|
||
if (best?.Score > 0)
|
||
{
|
||
reason = $"Shared first {best.Score} resource token(s).";
|
||
return best.Path;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string GetExportedResourceNameWithoutExtension(string path)
|
||
{
|
||
var name = Path.GetFileNameWithoutExtension(path);
|
||
var firstUnderscore = name.IndexOf('_');
|
||
// Экспортер часто добавляет numeric prefix: 58_MESH_..., 81_WEAR_...
|
||
if (firstUnderscore > 0 && name[..firstUnderscore].All(char.IsDigit))
|
||
name = name[(firstUnderscore + 1)..];
|
||
|
||
return name;
|
||
}
|
||
|
||
private static string ExtractObjectStem(string resourceName)
|
||
{
|
||
var normalized = resourceName;
|
||
// Тип ресурса не является частью имени объекта, поэтому MESH_o_x и WEAR_o_x должны совпасть.
|
||
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;
|
||
}
|
||
}
|