feat(scanner): add native artifact pipeline
Add native IK-style capture processing, Artifact Inventory, explicit promotion and single-result review. Confirm the three live OCR corrections in the eval corpus and preserve extraction/value separation.
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace GenshinAssistant.InputHelper;
|
||||
|
||||
internal static class IkInventoryLists
|
||||
{
|
||||
public static IkInventoryListStatus Load(string dataDir)
|
||||
{
|
||||
var dir = ResolveDirectory(dataDir);
|
||||
var required = new[] { "artifacts.json", "weapons.json", "characters.json", "materials.json", "version.txt" };
|
||||
var missing = required.Where(file => !File.Exists(Path.Combine(dir, file))).ToArray();
|
||||
var status = new IkInventoryListStatus { Directory = dir, Missing = missing };
|
||||
if (missing.Length > 0) return status;
|
||||
|
||||
status.Version = File.ReadAllText(Path.Combine(dir, "version.txt")).Trim();
|
||||
status.ArtifactSets = CountJsonObjectProperties(Path.Combine(dir, "artifacts.json"));
|
||||
status.ArtifactPieces = CountArtifactPieces(Path.Combine(dir, "artifacts.json"));
|
||||
status.Weapons = CountJsonObjectProperties(Path.Combine(dir, "weapons.json"));
|
||||
status.Characters = CountJsonObjectProperties(Path.Combine(dir, "characters.json"));
|
||||
status.Materials = CountJsonObjectProperties(Path.Combine(dir, "materials.json"));
|
||||
return status;
|
||||
}
|
||||
|
||||
private static string ResolveDirectory(string dataDir)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(dataDir)) return dataDir;
|
||||
|
||||
var candidates = new List<string>();
|
||||
var envDir = Environment.GetEnvironmentVariable("IK_INVENTORYLISTS_DIR");
|
||||
if (!string.IsNullOrWhiteSpace(envDir)) candidates.Add(envDir);
|
||||
|
||||
AddDirectoryCandidates(candidates, AppContext.BaseDirectory);
|
||||
AddDirectoryCandidates(candidates, Environment.CurrentDirectory);
|
||||
|
||||
var baseParent = Directory.GetParent(AppContext.BaseDirectory);
|
||||
for (var depth = 0; depth < 6 && baseParent != null; depth++)
|
||||
{
|
||||
AddDirectoryCandidates(candidates, baseParent.FullName);
|
||||
baseParent = baseParent.Parent;
|
||||
}
|
||||
|
||||
return candidates.FirstOrDefault(IsCompleteInventoryListDirectory)
|
||||
?? Path.Combine(AppContext.BaseDirectory, "inventorylists");
|
||||
}
|
||||
|
||||
private static void AddDirectoryCandidates(List<string> candidates, string root)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(root)) return;
|
||||
candidates.Add(Path.Combine(root, "inventorylists"));
|
||||
candidates.Add(Path.Combine(root, "ik-inventorylists"));
|
||||
candidates.Add(Path.Combine(root, "data", "ik-inventorylists"));
|
||||
}
|
||||
|
||||
private static bool IsCompleteInventoryListDirectory(string dir)
|
||||
{
|
||||
return File.Exists(Path.Combine(dir, "artifacts.json"))
|
||||
&& File.Exists(Path.Combine(dir, "weapons.json"))
|
||||
&& File.Exists(Path.Combine(dir, "characters.json"))
|
||||
&& File.Exists(Path.Combine(dir, "materials.json"))
|
||||
&& File.Exists(Path.Combine(dir, "version.txt"));
|
||||
}
|
||||
|
||||
public static object CatalogPayload(string dataDir)
|
||||
{
|
||||
var status = Load(dataDir);
|
||||
if (!status.Valid)
|
||||
{
|
||||
return new
|
||||
{
|
||||
data = status.ToPayload(),
|
||||
artifacts = Array.Empty<object>(),
|
||||
weapons = Array.Empty<object>(),
|
||||
characters = Array.Empty<object>(),
|
||||
materials = Array.Empty<object>(),
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
data = status.ToPayload(),
|
||||
artifacts = LoadArtifactCatalog(Path.Combine(status.Directory, "artifacts.json")),
|
||||
weapons = LoadStringMapCatalog(Path.Combine(status.Directory, "weapons.json")),
|
||||
characters = LoadCharacterCatalog(Path.Combine(status.Directory, "characters.json")),
|
||||
materials = LoadStringMapCatalog(Path.Combine(status.Directory, "materials.json")),
|
||||
};
|
||||
}
|
||||
|
||||
private static int CountJsonObjectProperties(string path)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
return doc.RootElement.ValueKind == JsonValueKind.Object
|
||||
? doc.RootElement.EnumerateObject().Count()
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static int CountArtifactPieces(string path)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return 0;
|
||||
var count = 0;
|
||||
foreach (var set in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!set.Value.TryGetProperty("artifacts", out var artifacts)) continue;
|
||||
if (artifacts.ValueKind == JsonValueKind.Object) count += artifacts.EnumerateObject().Count();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static List<object> LoadStringMapCatalog(string path)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var entries = new List<object>();
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return entries;
|
||||
foreach (var entry in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
entries.Add(new
|
||||
{
|
||||
normalizedName = entry.Name,
|
||||
good = entry.Value.ValueKind == JsonValueKind.String ? entry.Value.GetString() ?? "" : "",
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<object> LoadCharacterCatalog(string path)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var entries = new List<object>();
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return entries;
|
||||
foreach (var entry in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
var value = entry.Value;
|
||||
entries.Add(new
|
||||
{
|
||||
normalizedName = entry.Name,
|
||||
good = JsonString(value, "GOOD"),
|
||||
element = JsonFirstString(value, "Element"),
|
||||
weaponType = JsonFirstInt(value, "WeaponType"),
|
||||
constellationName = JsonFirstString(value, "ConstellationName"),
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<object> LoadArtifactCatalog(string path)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var entries = new List<object>();
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return entries;
|
||||
foreach (var set in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
var pieces = new List<object>();
|
||||
if (set.Value.TryGetProperty("artifacts", out var artifacts) && artifacts.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var piece in artifacts.EnumerateObject())
|
||||
{
|
||||
pieces.Add(new
|
||||
{
|
||||
slot = piece.Name,
|
||||
artifactName = JsonString(piece.Value, "artifactName"),
|
||||
good = JsonString(piece.Value, "GOOD"),
|
||||
normalizedName = JsonString(piece.Value, "normalizedName"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entries.Add(new
|
||||
{
|
||||
normalizedName = set.Name,
|
||||
setName = JsonString(set.Value, "setName"),
|
||||
good = JsonString(set.Value, "GOOD"),
|
||||
pieces,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static string JsonString(JsonElement value, string propertyName)
|
||||
=> value.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String
|
||||
? property.GetString() ?? ""
|
||||
: "";
|
||||
|
||||
private static string JsonFirstString(JsonElement value, string propertyName)
|
||||
{
|
||||
if (!value.TryGetProperty(propertyName, out var property)) return "";
|
||||
if (property.ValueKind == JsonValueKind.String) return property.GetString() ?? "";
|
||||
if (property.ValueKind == JsonValueKind.Array && property.GetArrayLength() > 0)
|
||||
{
|
||||
var first = property[0];
|
||||
return first.ValueKind == JsonValueKind.String ? first.GetString() ?? "" : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static int JsonFirstInt(JsonElement value, string propertyName)
|
||||
{
|
||||
if (!value.TryGetProperty(propertyName, out var property)) return -1;
|
||||
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out var number)) return number;
|
||||
if (property.ValueKind == JsonValueKind.Array && property.GetArrayLength() > 0)
|
||||
{
|
||||
var first = property[0];
|
||||
return first.ValueKind == JsonValueKind.Number && first.TryGetInt32(out number) ? number : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class IkInventoryListStatus
|
||||
{
|
||||
public string Directory { get; init; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public int ArtifactSets { get; set; }
|
||||
public int ArtifactPieces { get; set; }
|
||||
public int Weapons { get; set; }
|
||||
public int Characters { get; set; }
|
||||
public int Materials { get; set; }
|
||||
public string[] Missing { get; init; } = Array.Empty<string>();
|
||||
public bool Valid => Missing.Length == 0;
|
||||
|
||||
public object SupportedCategoriesPayload() => new
|
||||
{
|
||||
artifacts = ArtifactCategoryPayload("artifacts.json", ArtifactSets, ArtifactPieces),
|
||||
weapons = SimpleCategoryPayload("weapons.json", Weapons),
|
||||
characters = SimpleCategoryPayload("characters.json", Characters),
|
||||
materials = SimpleCategoryPayload("materials.json", Materials),
|
||||
};
|
||||
|
||||
private object ArtifactCategoryPayload(string file, int setCount, int pieceCount)
|
||||
{
|
||||
var catalogAvailable = Valid;
|
||||
var nativeCaptureSupported = Valid;
|
||||
return new
|
||||
{
|
||||
file,
|
||||
setCount,
|
||||
pieceCount,
|
||||
supported = nativeCaptureSupported,
|
||||
catalogAvailable,
|
||||
nativeCaptureSupported,
|
||||
scanStatus = nativeCaptureSupported ? "native_capture" : "missing_data",
|
||||
};
|
||||
}
|
||||
|
||||
private object SimpleCategoryPayload(string file, int count)
|
||||
{
|
||||
var catalogAvailable = Valid;
|
||||
const bool nativeCaptureSupported = false;
|
||||
return new
|
||||
{
|
||||
file,
|
||||
count,
|
||||
supported = nativeCaptureSupported,
|
||||
catalogAvailable,
|
||||
nativeCaptureSupported,
|
||||
scanStatus = catalogAvailable ? "catalog_only" : "missing_data",
|
||||
};
|
||||
}
|
||||
|
||||
public object ToPayload() => new
|
||||
{
|
||||
directory = Directory,
|
||||
version = Version,
|
||||
artifactSets = ArtifactSets,
|
||||
artifactPieces = ArtifactPieces,
|
||||
weapons = Weapons,
|
||||
characters = Characters,
|
||||
materials = Materials,
|
||||
totalEntries = ArtifactPieces + Weapons + Characters + Materials,
|
||||
categories = SupportedCategoriesPayload(),
|
||||
missing = Missing,
|
||||
valid = Valid,
|
||||
source = "InventoryKamera inventorylists",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user