namespace TravelEar.Core; /// /// The bind step's bookkeeping (docs/KNOWN-HAZARDS.md 3.1, REQ-HAZARD-NO-PARTIAL-FIDELITY): /// the mod resolves every game symbol it will touch through one binder before installing any /// hook; if a single symbol is missing the whole mod stays off and says so in one log line that /// names every miss. Resolution is injected ( takes the lookup), so the /// all-or-nothing rule is unit-tested without the game. A lookup that throws counts as missing. /// public sealed class SymbolBinder { private readonly List _missing = new(); private int _resolved; /// Symbols that failed to resolve so far, in order. public IReadOnlyList Missing => _missing; /// Symbols that resolved so far. public int Resolved => _resolved; // [impl->REQ-HAZARD-NO-PARTIAL-FIDELITY] /// /// Resolves one symbol. Returns the lookup's result, or null (and records /// as missing) when the lookup returns null or throws. /// public T Resolve(string symbol, Func lookup) where T : class { T result = null; try { result = lookup(); } catch (Exception) { // Ambiguous, unloadable, or otherwise unresolvable: missing, never fatal here. } if (result is null) _missing.Add(symbol); else _resolved++; return result; } /// Records a symbol as present or missing when the caller already knows. public void Require(string symbol, bool present) { if (present) _resolved++; else _missing.Add(symbol); } // [impl->REQ-HAZARD-NO-PARTIAL-FIDELITY] /// /// The verdict: true when every symbol resolved. is the single log /// line to emit either way; with misses it is an error line naming each of them. /// public bool Complete(out string report) { if (_missing.Count > 0) { report = $"TravelEar disabled: {_missing.Count} game symbol(s) not found after a game update: {string.Join(", ", _missing)}"; return false; } report = $"Game symbols bound ({_resolved})."; return true; } }