mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
819 lines
27 KiB
C#
819 lines
27 KiB
C#
using System.Drawing;
|
|
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Shigure;
|
|
|
|
internal sealed record SpellSuggestion(long SpellId, string Name);
|
|
|
|
internal sealed record ItemSuggestion(long ItemId, string Name);
|
|
|
|
/// <summary>
|
|
/// 技能/物品名称与 ID 到图标的只读目录。完整目录只来自外置数据包;数据包缺失或
|
|
/// 损坏时,技能图标与 spellId 联想均不可用。仅技能旧包仍可加载技能,此时物品搜索库关闭。
|
|
/// </summary>
|
|
internal static class SpellIconCatalog
|
|
{
|
|
private static readonly object SyncRoot = new();
|
|
private static readonly Dictionary<long, Image> Icons = new();
|
|
private static readonly Dictionary<long, Image> ItemIcons = new();
|
|
private static readonly Dictionary<string, Image?> NamedIcons = new(StringComparer.Ordinal);
|
|
private static readonly Dictionary<string, long> RegisteredSpellIdsByName = new(StringComparer.Ordinal);
|
|
private static readonly Dictionary<long, string> RegisteredSpellNamesById = new();
|
|
private static readonly Dictionary<string, long> RegisteredItemIdsByName = new(StringComparer.Ordinal);
|
|
private static readonly Dictionary<long, string> RegisteredItemNamesById = new();
|
|
private static readonly Regex ItemReferenceRegex = new(
|
|
@"item:(\d+)",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
|
|
private static readonly Dictionary<long, string> SpellIdIconResources = new()
|
|
{
|
|
[35395] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.crusader-strike.png"
|
|
};
|
|
|
|
private static readonly Dictionary<string, string> NamedIconResources = new(StringComparer.Ordinal)
|
|
{
|
|
["银月城生命药水"] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.silvermoon-city-health-potion.png",
|
|
[ModuleSpecialActions.OneKeySpell] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.one-key-spell.png",
|
|
[ModuleSpecialActions.PauseSpell] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.pause.png",
|
|
[ModuleSpecialActions.FailedSpell] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.auto-insert-spell.png",
|
|
[ModuleSpecialActions.FailedItem] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.auto-insert-spell.png",
|
|
["鲁莽药水"] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.recklessness-potion.jpg",
|
|
["圣光潜力"] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.lights-potential.jpg",
|
|
["光注法力药水"] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.light-infused-mana-potion.jpg",
|
|
["十字军打击"] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.crusader-strike.png",
|
|
["停止施法"] = $"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.stop-casting.png"
|
|
};
|
|
|
|
private static readonly string LastRuleRowIconResource =
|
|
$"{typeof(SpellIconCatalog).Namespace}.Assets.Spell.last-rule-row.png";
|
|
|
|
private static SpellIconPackage? _package;
|
|
private static Dictionary<string, long> _spellIdsByName = new(StringComparer.Ordinal);
|
|
private static Dictionary<string, long> _itemIdsByName = new(StringComparer.Ordinal);
|
|
private static IReadOnlyList<SpellSuggestion> _suggestionsBySpellId = Array.Empty<SpellSuggestion>();
|
|
private static IReadOnlyList<ItemSuggestion> _suggestionsByItemId = Array.Empty<ItemSuggestion>();
|
|
|
|
static SpellIconCatalog()
|
|
{
|
|
PromotePendingPackage();
|
|
_package = SpellIconPackage.TryOpen(PackagePath);
|
|
RebuildIndexesLocked();
|
|
}
|
|
|
|
internal static event Action? CatalogChanged;
|
|
|
|
internal static string PackagePath => Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"data",
|
|
"SpellIcons.shgpack");
|
|
|
|
internal static string PendingPackagePath => $"{PackagePath}.pending";
|
|
|
|
internal static bool IsPackageAvailable
|
|
{
|
|
get
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return _package is not null;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static bool IsItemDatabaseAvailable
|
|
{
|
|
get
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return _package is { HasItemDatabase: true };
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void PromotePendingPackage()
|
|
{
|
|
if (!File.Exists(PendingPackagePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
using (SpellIconPackage.Open(PendingPackagePath))
|
|
{
|
|
}
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(PackagePath)!);
|
|
File.Move(PendingPackagePath, PackagePath, overwrite: true);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
|
|
or InvalidDataException or ArgumentException)
|
|
{
|
|
// 保留当前可用数据包;待下次启动时再次尝试应用已校验的名称修复包。
|
|
}
|
|
}
|
|
|
|
public static Image? Get(long spellId)
|
|
{
|
|
if (spellId <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
if (_package is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (Icons.TryGetValue(spellId, out var cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
Image? icon = null;
|
|
if (SpellIdIconResources.TryGetValue(spellId, out var resourceName))
|
|
{
|
|
icon = LoadResource(resourceName);
|
|
}
|
|
|
|
icon ??= _package.LoadSpellIcon(spellId);
|
|
return icon is null ? null : CacheLocked(Icons, spellId, icon);
|
|
}
|
|
}
|
|
|
|
public static Image? Get(string? spellName)
|
|
{
|
|
var normalized = spellName?.Trim();
|
|
if (string.IsNullOrWhiteSpace(normalized))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
if (_package is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (NamedIconResources.TryGetValue(normalized, out var resourceName))
|
|
{
|
|
return GetNamedIconLocked(normalized, resourceName);
|
|
}
|
|
|
|
return _spellIdsByName.TryGetValue(normalized, out var spellId)
|
|
? Get(spellId)
|
|
: null;
|
|
}
|
|
}
|
|
|
|
public static Image? GetItem(long itemId)
|
|
{
|
|
if (itemId <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
if (_package is not { HasItemDatabase: true })
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (ItemIcons.TryGetValue(itemId, out var cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
var icon = _package.LoadItemIcon(itemId);
|
|
return icon is null ? null : CacheLocked(ItemIcons, itemId, icon);
|
|
}
|
|
}
|
|
|
|
public static Image? GetItem(string? itemName)
|
|
{
|
|
return TryResolveItemId(itemName, out var itemId)
|
|
? GetItem(itemId)
|
|
: null;
|
|
}
|
|
|
|
public static void Register(long spellId, string? spellName, bool overwriteIdName = false)
|
|
{
|
|
var normalized = spellName?.Trim();
|
|
if (spellId <= 0 || string.IsNullOrWhiteSpace(normalized))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
RegisteredSpellIdsByName[normalized] = spellId;
|
|
if (overwriteIdName)
|
|
{
|
|
RegisteredSpellNamesById[spellId] = normalized;
|
|
}
|
|
else
|
|
{
|
|
RegisteredSpellNamesById.TryAdd(spellId, normalized);
|
|
}
|
|
|
|
_spellIdsByName[normalized] = spellId;
|
|
}
|
|
}
|
|
|
|
public static void RegisterItem(long itemId, string? itemName)
|
|
{
|
|
var normalized = itemName?.Trim();
|
|
if (itemId <= 0 || string.IsNullOrWhiteSpace(normalized))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
RegisteredItemIdsByName[normalized] = itemId;
|
|
RegisteredItemNamesById.TryAdd(itemId, normalized);
|
|
_itemIdsByName[normalized] = itemId;
|
|
}
|
|
}
|
|
|
|
internal static string? ResolveSuggestionName(long spellId, string? packageName)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(packageName))
|
|
{
|
|
return packageName;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
return RegisteredSpellNamesById.GetValueOrDefault(spellId);
|
|
}
|
|
}
|
|
|
|
internal static string? ResolveItemSuggestionName(long itemId, string? packageName)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(packageName))
|
|
{
|
|
return packageName;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
return RegisteredItemNamesById.GetValueOrDefault(itemId)
|
|
?? _package?.ItemNamesById.GetValueOrDefault(itemId);
|
|
}
|
|
}
|
|
|
|
internal static IReadOnlyDictionary<long, string> GetRegisteredSpellNamesSnapshot()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return new Dictionary<long, string>(RegisteredSpellNamesById);
|
|
}
|
|
}
|
|
|
|
internal static IReadOnlyDictionary<long, string> GetRegisteredItemNamesSnapshot()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return new Dictionary<long, string>(RegisteredItemNamesById);
|
|
}
|
|
}
|
|
|
|
internal static IReadOnlyList<SpellSuggestion> GetSuggestionsSnapshot()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return _package is null
|
|
? Array.Empty<SpellSuggestion>()
|
|
: _suggestionsBySpellId;
|
|
}
|
|
}
|
|
|
|
internal static IReadOnlyList<ItemSuggestion> GetItemSuggestionsSnapshot()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return _package is { HasItemDatabase: true }
|
|
? _suggestionsByItemId
|
|
: Array.Empty<ItemSuggestion>();
|
|
}
|
|
}
|
|
|
|
internal static bool TryResolveItemId(string? text, out long itemId)
|
|
{
|
|
itemId = 0;
|
|
var normalized = text?.Trim();
|
|
if (string.IsNullOrWhiteSpace(normalized))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (TryParseItemReference(normalized, out itemId))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
return _itemIdsByName.TryGetValue(normalized, out itemId);
|
|
}
|
|
}
|
|
|
|
internal static bool TryParseItemReference(string? text, out long itemId)
|
|
{
|
|
itemId = 0;
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var match = ItemReferenceRegex.Match(text);
|
|
return match.Success
|
|
&& long.TryParse(
|
|
match.Groups[1].Value,
|
|
NumberStyles.None,
|
|
CultureInfo.InvariantCulture,
|
|
out itemId)
|
|
&& itemId > 0;
|
|
}
|
|
|
|
public static Image? GetLastRuleRowIcon()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
return _package is null
|
|
? null
|
|
: GetNamedIconLocked("last-rule-row", LastRuleRowIconResource);
|
|
}
|
|
}
|
|
|
|
internal static void ValidatePackage(string path)
|
|
{
|
|
using var package = SpellIconPackage.Open(path);
|
|
}
|
|
|
|
internal static void InstallPackage(string downloadedPath)
|
|
{
|
|
ValidatePackage(downloadedPath);
|
|
|
|
var targetPath = PackagePath;
|
|
var targetExisted = File.Exists(targetPath);
|
|
var backupPath = $"{targetPath}.{Guid.NewGuid():N}.backup";
|
|
Exception? failure = null;
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
var oldPackage = _package;
|
|
_package = null;
|
|
oldPackage?.Dispose();
|
|
DisposeImageCachesLocked();
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
|
if (targetExisted)
|
|
{
|
|
File.Replace(downloadedPath, targetPath, backupPath, ignoreMetadataErrors: true);
|
|
}
|
|
else
|
|
{
|
|
File.Move(downloadedPath, targetPath);
|
|
}
|
|
|
|
_package = SpellIconPackage.Open(targetPath);
|
|
RebuildIndexesLocked();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failure = ex;
|
|
TryRestorePreviousPackage(targetPath, backupPath, targetExisted);
|
|
_package = SpellIconPackage.TryOpen(targetPath);
|
|
RebuildIndexesLocked();
|
|
}
|
|
}
|
|
|
|
TryDeleteFile(backupPath);
|
|
CatalogChanged?.Invoke();
|
|
|
|
if (failure is not null)
|
|
{
|
|
throw new IOException("安装技能/物品图标数据包失败,已尝试恢复原数据包。", failure);
|
|
}
|
|
}
|
|
|
|
private static void TryRestorePreviousPackage(string targetPath, string backupPath, bool targetExisted)
|
|
{
|
|
try
|
|
{
|
|
if (targetExisted && File.Exists(backupPath))
|
|
{
|
|
File.Move(backupPath, targetPath, overwrite: true);
|
|
}
|
|
else if (!targetExisted)
|
|
{
|
|
TryDeleteFile(targetPath);
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
// 后续重新打开会把无法恢复的状态视为“未安装”。
|
|
}
|
|
}
|
|
|
|
private static void TryDeleteFile(string path)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
{
|
|
// 临时/备份文件清理失败不覆盖主要操作结果。
|
|
}
|
|
}
|
|
|
|
private static Image CacheLocked(Dictionary<long, Image> cache, long id, Image icon)
|
|
{
|
|
if (cache.TryGetValue(id, out var cached))
|
|
{
|
|
icon.Dispose();
|
|
return cached;
|
|
}
|
|
|
|
cache[id] = icon;
|
|
return icon;
|
|
}
|
|
|
|
private static Image? GetNamedIconLocked(string cacheKey, string resourceName)
|
|
{
|
|
if (NamedIcons.TryGetValue(cacheKey, out var cached))
|
|
{
|
|
return cached;
|
|
}
|
|
|
|
var icon = LoadResource(resourceName);
|
|
NamedIcons[cacheKey] = icon;
|
|
return icon;
|
|
}
|
|
|
|
private static Image? LoadResource(string resourceName)
|
|
{
|
|
using var stream = typeof(SpellIconCatalog).Assembly.GetManifestResourceStream(resourceName);
|
|
if (stream is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var source = Image.FromStream(stream);
|
|
return new Bitmap(source);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static void RebuildIndexesLocked()
|
|
{
|
|
_spellIdsByName = new Dictionary<string, long>(RegisteredSpellIdsByName, StringComparer.Ordinal);
|
|
_itemIdsByName = new Dictionary<string, long>(RegisteredItemIdsByName, StringComparer.Ordinal);
|
|
if (_package is null)
|
|
{
|
|
_suggestionsBySpellId = Array.Empty<SpellSuggestion>();
|
|
_suggestionsByItemId = Array.Empty<ItemSuggestion>();
|
|
return;
|
|
}
|
|
|
|
foreach (var (name, spellId) in _package.SpellIdsByName)
|
|
{
|
|
_spellIdsByName.TryAdd(name, spellId);
|
|
}
|
|
|
|
_suggestionsBySpellId = Array.AsReadOnly(
|
|
_package.SpellIds
|
|
.Select(spellId => new SpellSuggestion(
|
|
spellId,
|
|
_package.SpellNamesById.GetValueOrDefault(spellId) ?? string.Empty))
|
|
.ToArray());
|
|
|
|
if (!_package.HasItemDatabase)
|
|
{
|
|
_suggestionsByItemId = Array.Empty<ItemSuggestion>();
|
|
return;
|
|
}
|
|
|
|
foreach (var (name, itemId) in _package.ItemIdsByName)
|
|
{
|
|
_itemIdsByName.TryAdd(name, itemId);
|
|
}
|
|
|
|
_suggestionsByItemId = Array.AsReadOnly(
|
|
_package.ItemIds
|
|
.Select(itemId => new ItemSuggestion(
|
|
itemId,
|
|
_package.ItemNamesById.GetValueOrDefault(itemId) ?? string.Empty))
|
|
.ToArray());
|
|
}
|
|
|
|
private static void DisposeImageCachesLocked()
|
|
{
|
|
foreach (var image in Icons.Values)
|
|
{
|
|
image.Dispose();
|
|
}
|
|
|
|
Icons.Clear();
|
|
foreach (var image in ItemIcons.Values)
|
|
{
|
|
image.Dispose();
|
|
}
|
|
|
|
ItemIcons.Clear();
|
|
foreach (var image in NamedIcons.Values)
|
|
{
|
|
image?.Dispose();
|
|
}
|
|
|
|
NamedIcons.Clear();
|
|
}
|
|
|
|
private sealed class SpellIconPackage : IDisposable
|
|
{
|
|
private static readonly byte[] Magic = "SHGICN1\0"u8.ToArray();
|
|
private static readonly byte[] ItemFooterMagic = "SHGITM1\0"u8.ToArray();
|
|
private const int Version = 1;
|
|
private const int HeaderSize = 56;
|
|
private const int RecordSize = 12;
|
|
private const int ItemFooterSize = 48;
|
|
|
|
private readonly FileStream _stream;
|
|
private readonly long[] _spellIds;
|
|
private readonly int[] _spellIconIndices;
|
|
private readonly long[] _itemIds;
|
|
private readonly int[] _itemIconIndices;
|
|
private readonly long[] _iconOffsets;
|
|
private readonly int[] _iconLengths;
|
|
|
|
private SpellIconPackage(string path)
|
|
{
|
|
_stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
try
|
|
{
|
|
using var reader = new BinaryReader(_stream, System.Text.Encoding.UTF8, leaveOpen: true);
|
|
if (!reader.ReadBytes(Magic.Length).SequenceEqual(Magic)
|
|
|| reader.ReadInt32() != Version)
|
|
{
|
|
throw new InvalidDataException("Unsupported spell icon package.");
|
|
}
|
|
|
|
var spellCount = reader.ReadInt32();
|
|
var iconCount = reader.ReadInt32();
|
|
var nameCount = reader.ReadInt32();
|
|
var spellMapOffset = reader.ReadInt64();
|
|
var iconIndexOffset = reader.ReadInt64();
|
|
var nameIndexOffset = reader.ReadInt64();
|
|
var dataOffset = reader.ReadInt64();
|
|
if (spellCount is < 1 or > 2_000_000
|
|
|| iconCount is < 1 or > 100_000
|
|
|| nameCount is < 0 or > 2_000_000
|
|
|| spellMapOffset != HeaderSize
|
|
|| iconIndexOffset != spellMapOffset + (long)spellCount * RecordSize
|
|
|| nameIndexOffset != iconIndexOffset + (long)iconCount * RecordSize
|
|
|| dataOffset < nameIndexOffset
|
|
|| dataOffset > _stream.Length)
|
|
{
|
|
throw new InvalidDataException("Invalid spell icon package header.");
|
|
}
|
|
|
|
_spellIds = new long[spellCount];
|
|
_spellIconIndices = new int[spellCount];
|
|
_stream.Position = spellMapOffset;
|
|
for (var index = 0; index < spellCount; index++)
|
|
{
|
|
var spellId = reader.ReadInt64();
|
|
var iconIndex = reader.ReadInt32();
|
|
if (spellId <= 0
|
|
|| index > 0 && spellId <= _spellIds[index - 1]
|
|
|| iconIndex < 0
|
|
|| iconIndex >= iconCount)
|
|
{
|
|
throw new InvalidDataException("Invalid spell map in icon package.");
|
|
}
|
|
|
|
_spellIds[index] = spellId;
|
|
_spellIconIndices[index] = iconIndex;
|
|
}
|
|
|
|
_iconOffsets = new long[iconCount];
|
|
_iconLengths = new int[iconCount];
|
|
_stream.Position = iconIndexOffset;
|
|
for (var index = 0; index < iconCount; index++)
|
|
{
|
|
var offset = reader.ReadInt64();
|
|
var length = reader.ReadInt32();
|
|
if (offset < dataOffset
|
|
|| length is < 512 or > 10 * 1024 * 1024
|
|
|| offset > _stream.Length - length)
|
|
{
|
|
throw new InvalidDataException("Invalid image index in icon package.");
|
|
}
|
|
|
|
_iconOffsets[index] = offset;
|
|
_iconLengths[index] = length;
|
|
}
|
|
|
|
SpellIdsByName = new Dictionary<string, long>(StringComparer.Ordinal);
|
|
SpellNamesById = new Dictionary<long, string>();
|
|
_stream.Position = nameIndexOffset;
|
|
for (var index = 0; index < nameCount; index++)
|
|
{
|
|
var spellId = reader.ReadInt64();
|
|
var byteLength = reader.ReadInt32();
|
|
if (spellId <= 0
|
|
|| byteLength is < 1 or > 4096
|
|
|| _stream.Position > dataOffset - byteLength)
|
|
{
|
|
throw new InvalidDataException("Invalid name index in icon package.");
|
|
}
|
|
|
|
var name = System.Text.Encoding.UTF8.GetString(reader.ReadBytes(byteLength));
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
{
|
|
SpellIdsByName.TryAdd(name, spellId);
|
|
SpellNamesById.TryAdd(spellId, name);
|
|
}
|
|
}
|
|
|
|
if (_stream.Position != dataOffset)
|
|
{
|
|
throw new InvalidDataException("Spell icon package index size mismatch.");
|
|
}
|
|
|
|
ItemIdsByName = new Dictionary<string, long>(StringComparer.Ordinal);
|
|
ItemNamesById = new Dictionary<long, string>();
|
|
if (!TryReadItemExtension(reader, dataOffset, iconCount, out _itemIds, out _itemIconIndices))
|
|
{
|
|
_itemIds = [];
|
|
_itemIconIndices = [];
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_stream.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public Dictionary<string, long> SpellIdsByName { get; }
|
|
public Dictionary<long, string> SpellNamesById { get; }
|
|
public Dictionary<string, long> ItemIdsByName { get; }
|
|
public Dictionary<long, string> ItemNamesById { get; }
|
|
public IReadOnlyList<long> SpellIds => _spellIds;
|
|
public IReadOnlyList<long> ItemIds => _itemIds;
|
|
public bool HasItemDatabase => _itemIds.Length > 0;
|
|
|
|
public static SpellIconPackage Open(string path) => new(path);
|
|
|
|
public static SpellIconPackage? TryOpen(string path)
|
|
{
|
|
try
|
|
{
|
|
return File.Exists(path) ? Open(path) : null;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
|
|
or InvalidDataException or ArgumentException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public Image? LoadSpellIcon(long spellId)
|
|
{
|
|
var spellIndex = Array.BinarySearch(_spellIds, spellId);
|
|
return spellIndex < 0 ? null : LoadIconBlob(_spellIconIndices[spellIndex]);
|
|
}
|
|
|
|
public Image? LoadItemIcon(long itemId)
|
|
{
|
|
var itemIndex = Array.BinarySearch(_itemIds, itemId);
|
|
return itemIndex < 0 ? null : LoadIconBlob(_itemIconIndices[itemIndex]);
|
|
}
|
|
|
|
public void Dispose() => _stream.Dispose();
|
|
|
|
private bool TryReadItemExtension(
|
|
BinaryReader reader,
|
|
long dataOffset,
|
|
int iconCount,
|
|
out long[] itemIds,
|
|
out int[] itemIconIndices)
|
|
{
|
|
itemIds = [];
|
|
itemIconIndices = [];
|
|
if (_stream.Length < dataOffset + ItemFooterSize)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var footerOffset = _stream.Length - ItemFooterSize;
|
|
if (footerOffset < dataOffset)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_stream.Position = footerOffset;
|
|
var magic = reader.ReadBytes(ItemFooterMagic.Length);
|
|
if (!magic.SequenceEqual(ItemFooterMagic))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var itemCount = reader.ReadInt32();
|
|
var itemNameCount = reader.ReadInt32();
|
|
var itemMapOffset = reader.ReadInt64();
|
|
var itemNameOffset = reader.ReadInt64();
|
|
_ = reader.ReadInt64();
|
|
_ = reader.ReadInt64();
|
|
if (itemCount is < 1 or > 2_000_000
|
|
|| itemNameCount is < 0 or > 2_000_000
|
|
|| itemMapOffset < dataOffset
|
|
|| itemNameOffset != itemMapOffset + (long)itemCount * RecordSize
|
|
|| itemNameOffset > footerOffset)
|
|
{
|
|
throw new InvalidDataException("Invalid item extension footer in icon package.");
|
|
}
|
|
|
|
itemIds = new long[itemCount];
|
|
itemIconIndices = new int[itemCount];
|
|
_stream.Position = itemMapOffset;
|
|
for (var index = 0; index < itemCount; index++)
|
|
{
|
|
var itemId = reader.ReadInt64();
|
|
var iconIndex = reader.ReadInt32();
|
|
if (itemId <= 0
|
|
|| index > 0 && itemId <= itemIds[index - 1]
|
|
|| iconIndex < 0
|
|
|| iconIndex >= iconCount)
|
|
{
|
|
throw new InvalidDataException("Invalid item map in icon package.");
|
|
}
|
|
|
|
itemIds[index] = itemId;
|
|
itemIconIndices[index] = iconIndex;
|
|
}
|
|
|
|
_stream.Position = itemNameOffset;
|
|
for (var index = 0; index < itemNameCount; index++)
|
|
{
|
|
var itemId = reader.ReadInt64();
|
|
var byteLength = reader.ReadInt32();
|
|
if (itemId <= 0
|
|
|| byteLength is < 1 or > 4096
|
|
|| _stream.Position > footerOffset - byteLength)
|
|
{
|
|
throw new InvalidDataException("Invalid item name index in icon package.");
|
|
}
|
|
|
|
var name = System.Text.Encoding.UTF8.GetString(reader.ReadBytes(byteLength));
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
{
|
|
ItemIdsByName.TryAdd(name, itemId);
|
|
ItemNamesById.TryAdd(itemId, name);
|
|
}
|
|
}
|
|
|
|
if (_stream.Position != footerOffset)
|
|
{
|
|
throw new InvalidDataException("Item name table size mismatch in icon package.");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private Image? LoadIconBlob(int iconIndex)
|
|
{
|
|
var bytes = new byte[_iconLengths[iconIndex]];
|
|
try
|
|
{
|
|
_stream.Position = _iconOffsets[iconIndex];
|
|
_stream.ReadExactly(bytes);
|
|
using var memory = new MemoryStream(bytes, writable: false);
|
|
using var source = Image.FromStream(memory);
|
|
return new Bitmap(source);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or ArgumentException or ObjectDisposedException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|