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;
}
}