using System; using System.Collections.Generic; using System.IO; namespace finder2e_foundry_converter.Converters { public struct Result { public T Value { get; } public string? Error { get; } public bool IsSuccess => Error == null; private Result(T value, string? error) { Value = value; Error = error; } public static Result Success(T value) => new Result(value, null); public static Result Failure(string error) => new Result(default!, error); } public static class NpcConverter { private static Dictionary ParseToml(string content) { var root = new Dictionary(); var currentTable = root; var lines = content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); foreach (var rawLine in lines) { var line = rawLine.Trim(); if (string.IsNullOrEmpty(line) || line.StartsWith("#")) continue; if (line.StartsWith("[") && line.EndsWith("]")) { var section = line.Substring(1, line.Length - 2).Trim(); var parts = section.Split('.'); currentTable = root; foreach (var p in parts) { var part = p.Trim().Trim('"'); if (!currentTable.TryGetValue(part, out var next) || !(next is Dictionary)) { var newTable = new Dictionary(); currentTable[part] = newTable; currentTable = newTable; } else { currentTable = (Dictionary)next; } } } else if (line.Contains("=")) { var idx = line.IndexOf('='); var key = line.Substring(0, idx).Trim().Trim('"'); var valStr = line.Substring(idx + 1).Trim(); if (string.IsNullOrEmpty(key)) continue; currentTable[key] = ParseTomlValue(valStr); } } return root; } private static object ParseTomlValue(string valStr) { if (valStr.StartsWith("{") && valStr.EndsWith("}")) { var inlineTable = new Dictionary(); var content = valStr.Substring(1, valStr.Length - 2).Trim(); if (!string.IsNullOrEmpty(content)) { var pairs = content.Split(','); foreach (var pair in pairs) { var idx = pair.IndexOf('='); if (idx >= 0) { var k = pair.Substring(0, idx).Trim().Trim('"'); var vStr = pair.Substring(idx + 1).Trim(); inlineTable[k] = ParseTomlValue(vStr); } } } return inlineTable; } if (valStr.StartsWith("\"") && valStr.EndsWith("\"")) { return valStr.Substring(1, valStr.Length - 2); } if (long.TryParse(valStr, out var num)) { return num; } return valStr; } private static Result> LoadTableDocument(string normalizedSystem) { string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config", $"{normalizedSystem}_tables.toml"); try { if (!File.Exists(path)) { // Fallback attempt: sometimes config might be in working directory during dev var alternativePath = Path.Combine(Directory.GetCurrentDirectory(), "config", $"{normalizedSystem}_tables.toml"); if (File.Exists(alternativePath)) { path = alternativePath; } } string rawContents = File.ReadAllText(path); try { var document = ParseToml(rawContents); return Result>.Success(document); } catch (Exception ex) { return Result>.Failure($"failed to parse {path}: {ex.Message}"); } } catch (Exception ex) { return Result>.Failure($"failed to read {path}: {ex.Message}"); } } private static object? Navigate(Dictionary doc, params string[] path) { object current = doc; foreach (var key in path) { if (current is Dictionary dict) { if (!dict.TryGetValue(key, out var next)) return null; current = next; } else { return null; } } return current; } public static Result AbilityScoreModifier(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported ability modifier tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "ability_modifiers", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no ability modifier entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"ability modifier value `{number}` is out of range for i32"); } return Result.Success((int)number); } if (value is string text && text == "n/a") { return Result.Failure($"ability modifier tier `{normalizedTier}` is unavailable for level `{level}`"); } return Result.Failure($"invalid ability modifier value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result PerceptionModifier(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low" && normalizedTier != "terrible") { return Result.Failure($"unsupported perception tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, `low`, or `terrible`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "perception", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no perception entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"perception value `{number}` is out of range for i32"); } return Result.Success((int)number); } return Result.Failure($"invalid perception value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result SkillModifier(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported skill tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "skills", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no skill entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"skill value `{number}` is out of range for i32"); } return Result.Success((int)number); } if (value is Dictionary range && normalizedTier == "low") { if (!range.TryGetValue("minimum", out var minObj) || minObj is not long minLong) { return Result.Failure($"missing skill minimum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (!range.TryGetValue("maximum", out var maxObj) || maxObj is not long maxLong) { return Result.Failure($"missing skill maximum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (minLong < int.MinValue || minLong > int.MaxValue) { return Result.Failure($"skill minimum `{minLong}` is out of range for i32"); } if (maxLong < int.MinValue || maxLong > int.MaxValue) { return Result.Failure($"skill maximum `{maxLong}` is out of range for i32"); } int minimum = (int)minLong; int maximum = (int)maxLong; if (minimum > maximum) { return Result.Failure($"invalid skill range for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } return Result.Success(Random.Shared.Next(minimum, maximum + 1)); } return Result.Failure($"invalid skill value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result Ac(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported armor class tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "armor_class", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no armor class entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"armor class value `{number}` is out of range for i32"); } return Result.Success((int)number); } return Result.Failure($"invalid armor class value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result SavingThrow(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low" && normalizedTier != "terrible") { return Result.Failure($"unsupported saving throw tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, `low`, or `terrible`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "saving_throws", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no saving throw entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"saving throw value `{number}` is out of range for i32"); } return Result.Success((int)number); } return Result.Failure($"invalid saving throw value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result Hp(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported hit point tier `{normalizedTier}`; expected `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "hit_points", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no hit point entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is Dictionary range) { if (!range.TryGetValue("minimum", out var minObj) || minObj is not long minLong) { return Result.Failure($"missing hit point minimum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (!range.TryGetValue("maximum", out var maxObj) || maxObj is not long maxLong) { return Result.Failure($"missing hit point maximum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (minLong < int.MinValue || minLong > int.MaxValue) { return Result.Failure($"hit point minimum `{minLong}` is out of range for i32"); } if (maxLong < int.MinValue || maxLong > int.MaxValue) { return Result.Failure($"hit point maximum `{maxLong}` is out of range for i32"); } int minimum = (int)minLong; int maximum = (int)maxLong; if (minimum > maximum) { return Result.Failure($"invalid hit point range for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } return Result.Success(Random.Shared.Next(minimum, maximum + 1)); } return Result.Failure($"invalid hit point value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result ResistanceOrWeakness(int level, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var levelTable = Navigate(docResult.Value!, "resistances_and_weaknesses", "levels", level.ToString()); if (levelTable == null || levelTable is not Dictionary range) { return Result.Failure($"no resistance/weakness entry found for system `{normalizedSystem}`, level `{level}`"); } if (!range.TryGetValue("minimum", out var minObj) || minObj is not long minLong) { return Result.Failure($"missing resistance/weakness minimum for system `{normalizedSystem}`, level `{level}`"); } if (!range.TryGetValue("maximum", out var maxObj) || maxObj is not long maxLong) { return Result.Failure($"missing resistance/weakness maximum for system `{normalizedSystem}`, level `{level}`"); } if (minLong < int.MinValue || minLong > int.MaxValue) { return Result.Failure($"resistance/weakness minimum `{minLong}` is out of range for i32"); } if (maxLong < int.MinValue || maxLong > int.MaxValue) { return Result.Failure($"resistance/weakness maximum `{maxLong}` is out of range for i32"); } int minimum = (int)minLong; int maximum = (int)maxLong; if (minimum > maximum) { return Result.Failure($"invalid resistance/weakness range for system `{normalizedSystem}`, level `{level}`"); } return Result.Success(Random.Shared.Next(minimum, maximum + 1)); } public static Result StrikeAttackBonus(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported strike attack bonus tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "strike_attack_bonus", "levels", level.ToString(), normalizedTier); if (value == null) { return Result.Failure($"no strike attack bonus entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"strike attack bonus value `{number}` is out of range for i32"); } return Result.Success((int)number); } return Result.Failure($"invalid strike attack bonus value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result StrikeDamageRoll(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported strike damage tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "strike_damage", "levels", level.ToString(), normalizedTier); if (value == null || value is not Dictionary damageTable) { return Result.Failure($"no strike damage entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (damageTable.TryGetValue("dice", out var diceObj) && diceObj is string dice) { return Result.Success(dice); } return Result.Failure($"invalid strike damage roll for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result StrikeDamageAverage(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") { return Result.Failure($"unsupported strike damage tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "strike_damage", "levels", level.ToString(), normalizedTier); if (value == null || value is not Dictionary damageTable) { return Result.Failure($"no strike damage entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } if (damageTable.TryGetValue("average", out var avgObj) && avgObj is long average) { if (average < int.MinValue || average > int.MaxValue) { return Result.Failure($"strike damage average `{average}` is out of range for i32"); } return Result.Success((int)average); } return Result.Failure($"invalid strike damage average for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); } public static Result AreaDamageRoll(int level, bool unlimited, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string column = unlimited ? "unlimited" : "limited"; if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "area_damage", "levels", level.ToString(), column); if (value == null || value is not Dictionary damageTable) { return Result.Failure($"no area damage entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } if (damageTable.TryGetValue("dice", out var diceObj) && diceObj is string dice) { return Result.Success(dice); } return Result.Failure($"invalid area damage roll for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } public static Result AreaDamageAverage(int level, bool unlimited, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string column = unlimited ? "unlimited" : "limited"; if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "area_damage", "levels", level.ToString(), column); if (value == null || value is not Dictionary damageTable) { return Result.Failure($"no area damage entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } if (damageTable.TryGetValue("average", out var avgObj) && avgObj is long average) { if (average < int.MinValue || average > int.MaxValue) { return Result.Failure($"area damage average `{average}` is out of range for i32"); } return Result.Success((int)average); } return Result.Failure($"invalid area damage average for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } public static Result SpellDc(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); string column = normalizedTier switch { "extreme" => "extreme_dc", "high" => "high_dc", "moderate" => "moderate_dc", _ => null! }; if (column == null) { return Result.Failure($"unsupported spell dc tier `{normalizedTier}`; expected `extreme`, `high`, or `moderate`"); } if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "spellcasting", "levels", level.ToString(), column); if (value == null) { return Result.Failure($"no spell dc entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"spell dc value `{number}` is out of range for i32"); } return Result.Success((int)number); } return Result.Failure($"invalid spell dc value for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } public static Result SpellAttackBonus(int level, string modifierTier, string system) { string normalizedSystem = system.Trim().ToLowerInvariant(); string normalizedTier = modifierTier.Trim().ToLowerInvariant(); string column = normalizedTier switch { "extreme" => "extreme_spell_attack", "high" => "high_spell_attack", "moderate" => "moderate_spell_attack", _ => null! }; if (column == null) { return Result.Failure($"unsupported spell attack bonus tier `{normalizedTier}`; expected `extreme`, `high`, or `moderate`"); } if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") { return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); } var docResult = LoadTableDocument(normalizedSystem); if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); var value = Navigate(docResult.Value!, "spellcasting", "levels", level.ToString(), column); if (value == null) { return Result.Failure($"no spell attack bonus entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } if (value is long number) { if (number < int.MinValue || number > int.MaxValue) { return Result.Failure($"spell attack bonus value `{number}` is out of range for i32"); } return Result.Success((int)number); } return Result.Failure($"invalid spell attack bonus value for system `{normalizedSystem}`, level `{level}`, column `{column}`"); } } }