772 lines
29 KiB
C#
772 lines
29 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using NeoModLoader.services;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>One decision row from event-catalog.json (action phrase + EventStrength).</summary>
|
|
public sealed class DecisionCatalogRow
|
|
{
|
|
public string Id = "";
|
|
public string Action = "";
|
|
public float Strength;
|
|
}
|
|
|
|
/// <summary>Library dial defaults from event-catalog.json <c>libraries</c>.</summary>
|
|
public sealed class LibraryCatalogDefaults
|
|
{
|
|
public float AmbientStrength = 40f;
|
|
public float SignalStrength = 80f;
|
|
public string Category = "Event";
|
|
public string Label = "{a} · {id}";
|
|
public bool AmbientCreatesInterest = true;
|
|
public readonly HashSet<string> SignalIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads event-catalog.json — SoT for per-event strength/prose overlays.
|
|
/// <c>action</c> is decision-only (clause after "decides to"); other domains use <c>label</c> / happiness variants.
|
|
/// </summary>
|
|
public static class EventCatalogConfig
|
|
{
|
|
public const string FileName = "event-catalog.json";
|
|
public const float DefaultDecisionStrength = 86f;
|
|
|
|
private static readonly Dictionary<string, DecisionCatalogRow> Decisions =
|
|
new Dictionary<string, DecisionCatalogRow>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, float> SpeciesBonuses =
|
|
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, float> CharacterBonuses =
|
|
new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, HappinessCatalogEntry> Happiness =
|
|
new Dictionary<string, HappinessCatalogEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, StatusInterestEntry> Status =
|
|
new Dictionary<string, StatusInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, WorldLogEventEntry> WorldLog =
|
|
new Dictionary<string, WorldLogEventEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, DiscreteEventEntry> Plots =
|
|
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, DiscreteEventEntry> Relationship =
|
|
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, DiscreteEventEntry> Traits =
|
|
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, DiscreteEventEntry> Books =
|
|
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, DiscreteEventEntry> Eras =
|
|
new Dictionary<string, DiscreteEventEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, DisasterInterestEntry> Disasters =
|
|
new Dictionary<string, DisasterInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, WarTypeInterestEntry> WarTypes =
|
|
new Dictionary<string, WarTypeInterestEntry>(StringComparer.OrdinalIgnoreCase);
|
|
private static readonly Dictionary<string, LibraryCatalogDefaults> Libraries =
|
|
new Dictionary<string, LibraryCatalogDefaults>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static string _loadedPath = "";
|
|
private static string _loadedVersion = "";
|
|
private static bool _loadedFromFile;
|
|
private static int _generation;
|
|
|
|
public static bool LoadedFromFile => _loadedFromFile;
|
|
public static string LoadedPath => _loadedPath;
|
|
public static string LoadedVersion => _loadedVersion;
|
|
public static int Generation => _generation;
|
|
|
|
public static IEnumerable<string> DecisionIds => Decisions.Keys;
|
|
public static int DecisionCount => Decisions.Count;
|
|
public static int SpeciesCount => SpeciesBonuses.Count;
|
|
public static int CharacterCount => CharacterBonuses.Count;
|
|
public static int HappinessCount => Happiness.Count;
|
|
public static int StatusCount => Status.Count;
|
|
public static int WorldLogCount => WorldLog.Count;
|
|
public static int PlotCount => Plots.Count;
|
|
public static int RelationshipCount => Relationship.Count;
|
|
public static int TraitCount => Traits.Count;
|
|
public static int BookCount => Books.Count;
|
|
public static int EraCount => Eras.Count;
|
|
public static int DisasterCount => Disasters.Count;
|
|
public static int WarTypeCount => WarTypes.Count;
|
|
public static int LibraryCount => Libraries.Count;
|
|
|
|
public static void LoadFromModFolder(string modFolder)
|
|
{
|
|
ClearAll();
|
|
_loadedFromFile = false;
|
|
_loadedPath = "";
|
|
_loadedVersion = "";
|
|
|
|
if (string.IsNullOrEmpty(modFolder))
|
|
{
|
|
LogService.LogInfo("[IdleSpectator] event-catalog.json: no mod folder, using C# fallbacks");
|
|
BumpAndInvalidate();
|
|
return;
|
|
}
|
|
|
|
string path = Path.Combine(modFolder, FileName);
|
|
if (!File.Exists(path))
|
|
{
|
|
LogService.LogInfo($"[IdleSpectator] event-catalog.json missing at {path}, using C# fallbacks");
|
|
BumpAndInvalidate();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
string json = File.ReadAllText(path);
|
|
if (!TryParseDocument(json, out string version))
|
|
{
|
|
LogService.LogInfo("[IdleSpectator] event-catalog.json parse failed, using C# fallbacks");
|
|
ClearAll();
|
|
BumpAndInvalidate();
|
|
return;
|
|
}
|
|
|
|
_loadedFromFile = Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0;
|
|
_loadedPath = path;
|
|
_loadedVersion = version ?? "";
|
|
BumpAndInvalidate();
|
|
LogService.LogInfo(
|
|
$"[IdleSpectator] event-catalog.json loaded v={_loadedVersion} "
|
|
+ $"happiness={Happiness.Count} status={Status.Count} worldLog={WorldLog.Count} "
|
|
+ $"plots={Plots.Count} relationship={Relationship.Count} traits={Traits.Count} "
|
|
+ $"books={Books.Count} eras={Eras.Count} disasters={Disasters.Count} "
|
|
+ $"warTypes={WarTypes.Count} decisions={Decisions.Count} libraries={Libraries.Count} "
|
|
+ $"species={SpeciesBonuses.Count} character={CharacterBonuses.Count}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ClearAll();
|
|
LogService.LogInfo($"[IdleSpectator] event-catalog.json failed ({ex.Message}), using C# fallbacks");
|
|
BumpAndInvalidate();
|
|
}
|
|
}
|
|
|
|
private static void ClearAll()
|
|
{
|
|
Decisions.Clear();
|
|
SpeciesBonuses.Clear();
|
|
CharacterBonuses.Clear();
|
|
Happiness.Clear();
|
|
Status.Clear();
|
|
WorldLog.Clear();
|
|
Plots.Clear();
|
|
Relationship.Clear();
|
|
Traits.Clear();
|
|
Books.Clear();
|
|
Eras.Clear();
|
|
Disasters.Clear();
|
|
WarTypes.Clear();
|
|
Libraries.Clear();
|
|
}
|
|
|
|
private static void BumpAndInvalidate()
|
|
{
|
|
_generation++;
|
|
EventCatalog.InvalidateAllOverlays();
|
|
}
|
|
|
|
internal static bool TryParseDocument(string json, out string version)
|
|
{
|
|
version = "";
|
|
if (string.IsNullOrEmpty(json))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
version = ExtractJsonString(json, "modVersion") ?? "";
|
|
if (TryExtractArray(json, "decisions", out string a)) IngestDecisionObjects(a);
|
|
if (TryExtractArray(json, "species", out a)) IngestBonusObjects(a, SpeciesBonuses);
|
|
if (TryExtractArray(json, "character", out a)) IngestBonusObjects(a, CharacterBonuses);
|
|
if (TryExtractArray(json, "happiness", out a)) IngestHappiness(a);
|
|
if (TryExtractArray(json, "status", out a)) IngestStatus(a);
|
|
if (TryExtractArray(json, "worldLog", out a)) IngestWorldLog(a);
|
|
if (TryExtractArray(json, "plots", out a)) IngestDiscrete(a, Plots);
|
|
if (TryExtractArray(json, "relationship", out a)) IngestDiscrete(a, Relationship);
|
|
if (TryExtractArray(json, "traits", out a)) IngestDiscrete(a, Traits);
|
|
if (TryExtractArray(json, "books", out a)) IngestDiscrete(a, Books);
|
|
if (TryExtractArray(json, "eras", out a)) IngestDiscrete(a, Eras);
|
|
if (TryExtractArray(json, "disasters", out a)) IngestDisasters(a);
|
|
if (TryExtractArray(json, "warTypes", out a)) IngestWarTypes(a);
|
|
IngestLibraries(json);
|
|
|
|
return Happiness.Count > 0 || Decisions.Count > 0 || Status.Count > 0 || Plots.Count > 0;
|
|
}
|
|
|
|
public static int FillHappiness(Dictionary<string, HappinessCatalogEntry> into)
|
|
{
|
|
into.Clear();
|
|
foreach (KeyValuePair<string, HappinessCatalogEntry> kv in Happiness)
|
|
{
|
|
into[kv.Key] = kv.Value;
|
|
}
|
|
return into.Count;
|
|
}
|
|
|
|
public static int FillStatus(Dictionary<string, StatusInterestEntry> into)
|
|
{
|
|
into.Clear();
|
|
foreach (KeyValuePair<string, StatusInterestEntry> kv in Status)
|
|
{
|
|
into[kv.Key] = kv.Value;
|
|
}
|
|
return into.Count;
|
|
}
|
|
|
|
public static int FillWorldLog(Dictionary<string, WorldLogEventEntry> into)
|
|
{
|
|
into.Clear();
|
|
foreach (KeyValuePair<string, WorldLogEventEntry> kv in WorldLog)
|
|
{
|
|
into[kv.Key] = kv.Value;
|
|
}
|
|
return into.Count;
|
|
}
|
|
|
|
public static int FillPlots(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Plots, into);
|
|
public static int FillRelationship(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Relationship, into);
|
|
public static int FillTraits(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Traits, into);
|
|
public static int FillBooks(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Books, into);
|
|
public static int FillEras(Dictionary<string, DiscreteEventEntry> into) => FillDiscrete(Eras, into);
|
|
|
|
public static int FillDisasters(Dictionary<string, DisasterInterestEntry> into)
|
|
{
|
|
into.Clear();
|
|
foreach (KeyValuePair<string, DisasterInterestEntry> kv in Disasters)
|
|
{
|
|
into[kv.Key] = kv.Value;
|
|
}
|
|
return into.Count;
|
|
}
|
|
|
|
public static int FillWarTypes(Dictionary<string, WarTypeInterestEntry> into)
|
|
{
|
|
into.Clear();
|
|
foreach (KeyValuePair<string, WarTypeInterestEntry> kv in WarTypes)
|
|
{
|
|
into[kv.Key] = kv.Value;
|
|
}
|
|
return into.Count;
|
|
}
|
|
|
|
private static int FillDiscrete(
|
|
Dictionary<string, DiscreteEventEntry> src,
|
|
Dictionary<string, DiscreteEventEntry> into)
|
|
{
|
|
into.Clear();
|
|
foreach (KeyValuePair<string, DiscreteEventEntry> kv in src)
|
|
{
|
|
into[kv.Key] = kv.Value;
|
|
}
|
|
return into.Count;
|
|
}
|
|
|
|
public static bool TryGetLibraryDefaults(string key, out LibraryCatalogDefaults defaults)
|
|
{
|
|
defaults = null;
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
return false;
|
|
}
|
|
return Libraries.TryGetValue(key.Trim(), out defaults) && defaults != null;
|
|
}
|
|
|
|
private static void IngestHappiness(string arrayJson)
|
|
{
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
var entry = new HappinessCatalogEntry
|
|
{
|
|
Id = id.Trim(),
|
|
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
|
Presentation = ParseEnum(obj, "presentation", HappinessPresentationTier.Signal),
|
|
Context = ParseEnum(obj, "context", HappinessContextRequirement.None),
|
|
Life = ParseEnum(obj, "life", HappinessLifePolicy.ActivityOnly),
|
|
Overlap = ParseEnum(obj, "overlap", HappinessOverlapPolicy.Independent),
|
|
RelationRole = ParseEnum(obj, "relationRole", HappinessRelationRole.None),
|
|
InterestCategory = ExtractJsonString(obj, "category") ?? "",
|
|
CanonicalMilestoneKey = ExtractJsonString(obj, "canonicalMilestoneKey") ?? "",
|
|
StatusOverlapId = ExtractJsonString(obj, "statusOverlapId") ?? "",
|
|
StatusOverlapKind = ParseStatusOverlapKind(obj, ExtractJsonString(obj, "statusOverlapId")),
|
|
VariantsWithRelated = ExtractStringArray(obj, "variantsWithRelated"),
|
|
VariantsWithoutRelated = ExtractStringArray(obj, "variantsWithoutRelated")
|
|
};
|
|
Happiness[entry.Id] = entry;
|
|
}
|
|
}
|
|
|
|
private static void IngestStatus(string arrayJson)
|
|
{
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
Status[id.Trim()] = new StatusInterestEntry
|
|
{
|
|
Id = id.Trim(),
|
|
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
|
Category = ExtractJsonString(obj, "category") ?? "Status",
|
|
CreatesInterest = ExtractJsonBool(obj, "camera", false),
|
|
ExtendsGrief = ExtractJsonBool(obj, "extendsGrief", false)
|
|
};
|
|
}
|
|
}
|
|
|
|
private static void IngestWorldLog(string arrayJson)
|
|
{
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
bool camera = ExtractJsonBool(obj, "camera", true);
|
|
float strength = ExtractJsonFloat(obj, "strength", 40f);
|
|
bool chronicle = ExtractJsonBool(obj, "chronicle", camera && strength >= 65f);
|
|
WorldLog[id.Trim()] = new WorldLogEventEntry
|
|
{
|
|
Id = id.Trim(),
|
|
EventStrength = strength,
|
|
Category = ExtractJsonString(obj, "category") ?? "WorldLog",
|
|
CreatesInterest = camera,
|
|
ChronicleEligible = chronicle,
|
|
LabelTemplate = ExtractJsonString(obj, "label") ?? "{id}: {a}",
|
|
MinWatch = ExtractJsonFloat(obj, "minWatch", 6f),
|
|
MaxWatch = ExtractJsonFloat(obj, "maxWatch", 28f)
|
|
};
|
|
}
|
|
}
|
|
|
|
private static void IngestDiscrete(string arrayJson, Dictionary<string, DiscreteEventEntry> into)
|
|
{
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
into[id.Trim()] = new DiscreteEventEntry
|
|
{
|
|
Id = id.Trim(),
|
|
EventStrength = ExtractJsonFloat(obj, "strength", 40f),
|
|
Category = ExtractJsonString(obj, "category") ?? "Event",
|
|
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
|
LabelTemplate = ExtractJsonString(obj, "label") ?? "{a} · {id}",
|
|
ActionPhrase = ExtractJsonString(obj, "action") ?? ""
|
|
};
|
|
}
|
|
}
|
|
|
|
private static void IngestDisasters(string arrayJson)
|
|
{
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
Disasters[id.Trim()] = new DisasterInterestEntry
|
|
{
|
|
Id = id.Trim(),
|
|
EventStrength = ExtractJsonFloat(obj, "strength", 90f),
|
|
Category = ExtractJsonString(obj, "category") ?? "Disaster",
|
|
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
|
WorldLogId = ExtractJsonString(obj, "worldLogId") ?? ""
|
|
};
|
|
}
|
|
}
|
|
|
|
private static void IngestWarTypes(string arrayJson)
|
|
{
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
WarTypes[id.Trim()] = new WarTypeInterestEntry
|
|
{
|
|
Id = id.Trim(),
|
|
EventStrength = ExtractJsonFloat(obj, "strength", 85f),
|
|
Category = ExtractJsonString(obj, "category") ?? "Politics",
|
|
CreatesInterest = ExtractJsonBool(obj, "camera", true),
|
|
LabelTemplate = ExtractJsonString(obj, "label") ?? "War ({id})"
|
|
};
|
|
}
|
|
}
|
|
|
|
private static void IngestLibraries(string json)
|
|
{
|
|
if (!TryExtractObject(json, "libraries", out string libsJson))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string[] keys =
|
|
{
|
|
"spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome"
|
|
};
|
|
for (int i = 0; i < keys.Length; i++)
|
|
{
|
|
string key = keys[i];
|
|
if (!TryExtractObject(libsJson, key, out string obj))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var d = new LibraryCatalogDefaults
|
|
{
|
|
AmbientStrength = ExtractJsonFloat(obj, "ambientStrength", 40f),
|
|
SignalStrength = ExtractJsonFloat(obj, "signalStrength", 80f),
|
|
Category = ExtractJsonString(obj, "category") ?? "Event",
|
|
Label = ExtractJsonString(obj, "label") ?? "{a} · {id}",
|
|
AmbientCreatesInterest = ExtractJsonBool(obj, "ambientCreatesInterest", true)
|
|
};
|
|
string[] ids = ExtractStringArray(obj, "signalIds");
|
|
for (int j = 0; j < ids.Length; j++)
|
|
{
|
|
if (!string.IsNullOrEmpty(ids[j]))
|
|
{
|
|
d.SignalIds.Add(ids[j]);
|
|
}
|
|
}
|
|
Libraries[key] = d;
|
|
}
|
|
}
|
|
|
|
private static int IngestDecisionObjects(string arrayJson)
|
|
{
|
|
int n = 0;
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
string action = ExtractJsonString(obj, "action");
|
|
if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(action)) continue;
|
|
float strength = ExtractJsonFloat(obj, "strength", DefaultDecisionStrength);
|
|
Decisions[id.Trim()] = new DecisionCatalogRow
|
|
{
|
|
Id = id.Trim(),
|
|
Action = action,
|
|
Strength = strength > 0f ? strength : DefaultDecisionStrength
|
|
};
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
private static int IngestBonusObjects(string arrayJson, Dictionary<string, float> into)
|
|
{
|
|
int n = 0;
|
|
foreach (string obj in EnumerateObjects(arrayJson))
|
|
{
|
|
string id = ExtractJsonString(obj, "id");
|
|
if (string.IsNullOrEmpty(id)) continue;
|
|
into[id.Trim()] = ExtractJsonFloat(obj, "bonus", 0f);
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
private static TEnum ParseEnum<TEnum>(string json, string key, TEnum fallback) where TEnum : struct
|
|
{
|
|
string raw = ExtractJsonString(json, key);
|
|
if (string.IsNullOrEmpty(raw)) return fallback;
|
|
return Enum.TryParse(raw, true, out TEnum value) ? value : fallback;
|
|
}
|
|
|
|
private static HappinessStatusOverlapKind ParseStatusOverlapKind(string json, string statusOverlapId)
|
|
{
|
|
if (string.IsNullOrEmpty(statusOverlapId))
|
|
{
|
|
return HappinessStatusOverlapKind.None;
|
|
}
|
|
|
|
HappinessStatusOverlapKind parsed = ParseEnum(
|
|
json, "statusOverlapKind", HappinessStatusOverlapKind.None);
|
|
if (parsed != HappinessStatusOverlapKind.None)
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
// Legacy rows without statusOverlapKind: offset end-pings vs onset holds.
|
|
string id = ExtractJsonString(json, "id") ?? "";
|
|
if (IsLegacyOffsetOverlap(id))
|
|
{
|
|
return HappinessStatusOverlapKind.Offset;
|
|
}
|
|
|
|
return HappinessStatusOverlapKind.Hold;
|
|
}
|
|
|
|
private static bool IsLegacyOffsetOverlap(string effectId)
|
|
{
|
|
if (string.IsNullOrEmpty(effectId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch (effectId.Trim().ToLowerInvariant())
|
|
{
|
|
case "just_laughed":
|
|
case "just_sang":
|
|
case "just_swore":
|
|
case "just_cried":
|
|
case "just_had_tantrum":
|
|
case "just_inspired":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static string[] ExtractStringArray(string json, string key)
|
|
{
|
|
if (!TryExtractArray(json, key, out string arr) || string.IsNullOrEmpty(arr))
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
var list = new List<string>();
|
|
int i = 0;
|
|
while (i < arr.Length)
|
|
{
|
|
int q = arr.IndexOf('"', i);
|
|
if (q < 0) break;
|
|
int end = q + 1;
|
|
var sb = new StringBuilder();
|
|
while (end < arr.Length)
|
|
{
|
|
char c = arr[end++];
|
|
if (c == '\\' && end < arr.Length) { sb.Append(arr[end++]); continue; }
|
|
if (c == '"') break;
|
|
sb.Append(c);
|
|
}
|
|
list.Add(sb.ToString());
|
|
i = end;
|
|
}
|
|
return list.ToArray();
|
|
}
|
|
|
|
private static bool ExtractJsonBool(string json, string key, bool fallback)
|
|
{
|
|
string needle = "\"" + key + "\"";
|
|
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
|
if (keyAt < 0) return fallback;
|
|
int colon = json.IndexOf(':', keyAt + needle.Length);
|
|
if (colon < 0) return fallback;
|
|
int i = colon + 1;
|
|
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
|
if (i + 4 <= json.Length && string.Compare(json, i, "true", 0, 4, StringComparison.OrdinalIgnoreCase) == 0)
|
|
return true;
|
|
if (i + 5 <= json.Length && string.Compare(json, i, "false", 0, 5, StringComparison.OrdinalIgnoreCase) == 0)
|
|
return false;
|
|
return fallback;
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateObjects(string arrayJson)
|
|
{
|
|
if (string.IsNullOrEmpty(arrayJson)) yield break;
|
|
int i = 0;
|
|
while (i < arrayJson.Length)
|
|
{
|
|
int start = arrayJson.IndexOf('{', i);
|
|
if (start < 0) yield break;
|
|
int depth = 0;
|
|
bool inString = false;
|
|
bool escape = false;
|
|
for (int j = start; j < arrayJson.Length; j++)
|
|
{
|
|
char c = arrayJson[j];
|
|
if (inString)
|
|
{
|
|
if (escape) { escape = false; continue; }
|
|
if (c == '\\') { escape = true; continue; }
|
|
if (c == '"') inString = false;
|
|
continue;
|
|
}
|
|
if (c == '"') { inString = true; continue; }
|
|
if (c == '{') depth++;
|
|
else if (c == '}')
|
|
{
|
|
depth--;
|
|
if (depth == 0)
|
|
{
|
|
yield return arrayJson.Substring(start, j - start + 1);
|
|
i = j + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (depth != 0) yield break;
|
|
}
|
|
}
|
|
|
|
private static string ExtractJsonString(string json, string key)
|
|
{
|
|
string needle = "\"" + key + "\"";
|
|
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
|
if (keyAt < 0) return null;
|
|
int colon = json.IndexOf(':', keyAt + needle.Length);
|
|
if (colon < 0) return null;
|
|
int i = colon + 1;
|
|
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
|
if (i >= json.Length || json[i] != '"') return null;
|
|
i++;
|
|
var sb = new StringBuilder();
|
|
while (i < json.Length)
|
|
{
|
|
char c = json[i++];
|
|
if (c == '\\' && i < json.Length) { sb.Append(json[i++]); continue; }
|
|
if (c == '"') break;
|
|
sb.Append(c);
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static float ExtractJsonFloat(string json, string key, float fallback)
|
|
{
|
|
string needle = "\"" + key + "\"";
|
|
int keyAt = json.IndexOf(needle, StringComparison.Ordinal);
|
|
if (keyAt < 0) return fallback;
|
|
int colon = json.IndexOf(':', keyAt + needle.Length);
|
|
if (colon < 0) return fallback;
|
|
int i = colon + 1;
|
|
while (i < json.Length && char.IsWhiteSpace(json[i])) i++;
|
|
int start = i;
|
|
while (i < json.Length && (char.IsDigit(json[i]) || json[i] == '.' || json[i] == '-' || json[i] == '+'))
|
|
i++;
|
|
if (i <= start) return fallback;
|
|
string token = json.Substring(start, i - start);
|
|
return float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)
|
|
? value : fallback;
|
|
}
|
|
|
|
private static bool TryExtractArray(string json, string key, out string arrayJson)
|
|
{
|
|
arrayJson = null;
|
|
string keyColon = "\"" + key + "\"";
|
|
int searchFrom = 0;
|
|
int keyAt = -1;
|
|
while (true)
|
|
{
|
|
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
|
|
if (at < 0) break;
|
|
int after = at + keyColon.Length;
|
|
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
|
|
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
|
|
searchFrom = at + 1;
|
|
}
|
|
if (keyAt < 0) return false;
|
|
int bracket = json.IndexOf('[', keyAt + keyColon.Length);
|
|
if (bracket < 0) return false;
|
|
int depth = 0;
|
|
bool inString = false;
|
|
bool escape = false;
|
|
for (int i = bracket; i < json.Length; i++)
|
|
{
|
|
char c = json[i];
|
|
if (inString)
|
|
{
|
|
if (escape) { escape = false; continue; }
|
|
if (c == '\\') { escape = true; continue; }
|
|
if (c == '"') inString = false;
|
|
continue;
|
|
}
|
|
if (c == '"') { inString = true; continue; }
|
|
if (c == '[') depth++;
|
|
else if (c == ']')
|
|
{
|
|
depth--;
|
|
if (depth == 0)
|
|
{
|
|
arrayJson = json.Substring(bracket, i - bracket + 1);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool TryExtractObject(string json, string key, out string objectJson)
|
|
{
|
|
objectJson = null;
|
|
string keyColon = "\"" + key + "\"";
|
|
int searchFrom = 0;
|
|
int keyAt = -1;
|
|
while (true)
|
|
{
|
|
int at = json.IndexOf(keyColon, searchFrom, StringComparison.Ordinal);
|
|
if (at < 0) break;
|
|
int after = at + keyColon.Length;
|
|
while (after < json.Length && char.IsWhiteSpace(json[after])) after++;
|
|
if (after < json.Length && json[after] == ':') { keyAt = at; break; }
|
|
searchFrom = at + 1;
|
|
}
|
|
if (keyAt < 0) return false;
|
|
int brace = json.IndexOf('{', keyAt + keyColon.Length);
|
|
if (brace < 0) return false;
|
|
int depth = 0;
|
|
bool inString = false;
|
|
bool escape = false;
|
|
for (int i = brace; i < json.Length; i++)
|
|
{
|
|
char c = json[i];
|
|
if (inString)
|
|
{
|
|
if (escape) { escape = false; continue; }
|
|
if (c == '\\') { escape = true; continue; }
|
|
if (c == '"') inString = false;
|
|
continue;
|
|
}
|
|
if (c == '"') { inString = true; continue; }
|
|
if (c == '{') depth++;
|
|
else if (c == '}')
|
|
{
|
|
depth--;
|
|
if (depth == 0)
|
|
{
|
|
objectJson = json.Substring(brace, i - brace + 1);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static bool TryGetDecision(string id, out DecisionCatalogRow row)
|
|
{
|
|
row = null;
|
|
if (string.IsNullOrEmpty(id)) return false;
|
|
return Decisions.TryGetValue(id.Trim(), out row) && row != null;
|
|
}
|
|
|
|
public static bool TryGetDecisionAction(string id, out string action)
|
|
{
|
|
action = "";
|
|
if (!TryGetDecision(id, out DecisionCatalogRow row)) return false;
|
|
action = row.Action ?? "";
|
|
return !string.IsNullOrEmpty(action);
|
|
}
|
|
|
|
public static float DecisionStrengthOrDefault(string id, float fallback = DefaultDecisionStrength)
|
|
{
|
|
if (TryGetDecision(id, out DecisionCatalogRow row) && row.Strength > 0f) return row.Strength;
|
|
return fallback;
|
|
}
|
|
|
|
public static float SpeciesBonus(string speciesId)
|
|
{
|
|
if (string.IsNullOrEmpty(speciesId)) return 0f;
|
|
return SpeciesBonuses.TryGetValue(speciesId.Trim(), out float bonus) ? bonus : 0f;
|
|
}
|
|
|
|
public static float CharacterBonus(string roleId, float scoringModelFallback)
|
|
{
|
|
if (string.IsNullOrEmpty(roleId)) return scoringModelFallback;
|
|
if (CharacterBonuses.TryGetValue(roleId.Trim(), out float bonus)) return bonus;
|
|
return scoringModelFallback;
|
|
}
|
|
|
|
public static bool Reload()
|
|
{
|
|
string folder = ModClass.ModFolder;
|
|
if (string.IsNullOrEmpty(folder)) return false;
|
|
LoadFromModFolder(folder);
|
|
return _loadedFromFile;
|
|
}
|
|
}
|