{"record":{"id":"5e8b458efd990514","repo":"babalae/better-genshin-impact","slug":"ocr-5e8b45","errorCode":null,"errorMessage":"武器名称 OCR 结果为空。","messagePattern":"武器名称 OCR 结果为空。","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"BetterGenshinImpact/GameTask/CharacterDevelopment/WeaponNameMatcher.cs","lineNumber":51,"sourceCode":"    private static readonly Lazy<IReadOnlyList<string>> WeaponNames = new(LoadWeaponNames);\n\n    /// <summary>\n    /// 将 OCR 文本匹配为标准武器名称。名称表延迟加载且在进程内复用。\n    /// </summary>\n    public static WeaponNameMatch Match(string ocrText)\n    {\n        return MatchClosest(ocrText, WeaponNames.Value);\n    }\n\n    /// <summary>\n    /// 在给定名称表中选择编辑距离最小的名称，并判断该候选是否足够可信。\n    /// </summary>\n    internal static WeaponNameMatch MatchClosest(string ocrText, IReadOnlyList<string> weaponNames)\n    {\n        var normalizedText = ocrText.Trim();\n        if (string.IsNullOrWhiteSpace(normalizedText))\n        {\n            throw new InvalidOperationException(\"武器名称 OCR 结果为空。\");\n        }\n\n        if (weaponNames.Count == 0)\n        {\n            throw new InvalidDataException(\"武器名称表中没有可用的武器。\");\n        }\n\n        var candidates = weaponNames\n            .Select(name =>\n            {\n                var distance = LevenshteinDistance(normalizedText, name);\n                var maximumLength = Math.Max(normalizedText.Length, name.Length);\n                var similarity = 1d - (double)distance / maximumLength;\n                return new WeaponNameMatch(name, distance, similarity, false);\n            })\n            .OrderBy(match => match.Distance)\n            .ThenBy(match => match.Name, StringComparer.Ordinal)\n            .Take(2)","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/babalae/better-genshin-impact/blob/a7cb36712dcb409be610257d877fcea3597e9d6b/BetterGenshinImpact/GameTask/CharacterDevelopment/WeaponNameMatcher.cs#L33-L69","documentation":"`WeaponNameMatcher.MatchClosest` trims the OCR text and throws `InvalidOperationException` if it is null/empty/whitespace. Matching an empty string against the weapon table is meaningless (every distance equals the name length), so it refuses rather than return a garbage 'nearest'. The public `Match` delegates here after the lazy name table loads.","triggerScenarios":"Calling `Match(null)`, `Match(\"\")`, or `Match(\"   \")`; upstream OCR returning empty for the weapon-name region and that empty value reaching the matcher without a null/empty guard.","commonSituations":"The weapon-name ROI captured a blank region (misaligned, not yet rendered); a test calls `Match` with empty input; `ReadWeaponNameWithRetry`'s empty-text branch is bypassed by a direct call.","solutions":["Check `string.IsNullOrWhiteSpace(ocrText)` before calling `Match`, and retry/skip when empty (as `ReadWeaponNameWithRetry` already does).","Ensure the weapon-name ROI actually contains the weapon name line at the current resolution.","In tests, pass a non-empty OCR string or assert the throw explicitly."],"exampleFix":"// before\nvar match = WeaponNameMatcher.Match(ocrText); // throws if ocrText is empty\n\n// after\nif (string.IsNullOrWhiteSpace(ocrText)) { /* retry / skip */ return; }\nvar match = WeaponNameMatcher.Match(ocrText);","handlingStrategy":"validation","validationCode":"if (string.IsNullOrWhiteSpace(ocrText))\n    return; // or retry; ReadWeaponNameWithRetry already does this\nvar match = WeaponNameMatcher.Match(ocrText);","typeGuard":null,"tryCatchPattern":"try { var m = WeaponNameMatcher.Match(text); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"武器名称 OCR 结果为空\"))\n{ /* ROI captured blank; retry or realign weapon-name ROI */ }","preventionTips":["Guard string.IsNullOrWhiteSpace before calling Match.","Ensure the weapon-name ROI contains the weapon name line at the current resolution.","Let ReadWeaponNameWithRetry own the empty-handling; don't bypass it."],"tags":["argument-validation","ocr","weapon-name","levenshtein","character-development"],"backgroundTag":null,"analyzedSha":"a7cb36712dcb409be610257d877fcea3597e9d6b","analyzedAt":"2026-08-13T16:44:57.548Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}