using GDWeave; using GDWeave.Godot; using GDWeave.Modding; using static Teemaw.Calico.LexicalTransformer.Operation; namespace Teemaw.Calico.LexicalTransformer; /// /// An IScriptMod implementation that handles patching through the provided list of TransformationRules. /// /// IModInterface of the current mod. /// The name of this script mod. Used for logging. /// The GD res:// path of the script which will be patched. /// A list of patches to perform. Multiple descriptors with overlapping checks is not supported. public class TransformationRuleScriptMod( IModInterface mod, string name, string scriptPath, Func predicate, TransformationRule[] rules) : IScriptMod { public bool ShouldRun(string path) { if (path != scriptPath) return false; if (!predicate.Invoke()) { mod.Logger.Information($"[{name}] Predicate failed, not patching."); return false; } return true; } public IEnumerable Modify(string path, IEnumerable tokens) { var eligibleRules = rules.Where(rule => { var eligible = rule.Predicate(); if (!eligible) { mod.Logger.Information($"[{name}] Skipping patch {rule.Name}..."); } return eligible; }).ToList(); var transformers = eligibleRules.Select(rule => (Rule: rule, Waiter: rule.CreateMultiTokenWaiter(), Buffer: new List())) .ToList(); mod.Logger.Information($"[{name}] Patching {path}"); var patchOccurrences = eligibleRules.ToDictionary(r => r.Name, r => (Occurred: 0, Expected: r.Times)); var bufferAfterChecks = true; var stagingBuffer = new List(); stagingBuffer.AddRange(tokens); var transformedBuffer = new List(); foreach (var transformer in transformers) { var hasScopePattern = transformer.Rule.ScopePattern.Length > 0; var inScope = !hasScopePattern; uint? scopeIndent = null; var scopeWaiter = transformer.Rule.CreateMultiTokenWaiterForScope(); foreach (var token in stagingBuffer) { if (inScope && hasScopePattern) { // Try to find the scope's base indentation if (scopeIndent == null && token.Type == TokenType.Newline) { scopeIndent = token.AssociatedData ?? 0; } // Check if we should leave the scope else if (scopeIndent != null && token.Type == TokenType.Newline) { inScope = (token.AssociatedData ?? 0) >= scopeIndent; } } else if (hasScopePattern) { // We should never reach this case if the scope pattern has Length = 0. if (scopeWaiter.Check(token)) { scopeWaiter.Reset(); // Latch inScope to true if we match the scope pattern. inScope = true; } } if (!inScope) { transformedBuffer.Add(token); continue; } transformer.Waiter.Check(token); if (transformer.Waiter.Step == 0) { transformedBuffer.AddRange(transformer.Buffer); transformer.Buffer.Clear(); } else { if (transformer.Rule.Operation.RequiresBuffer()) { transformer.Buffer.Add(token); bufferAfterChecks = false; } if (transformer.Waiter.Matched) { transformer.Waiter.Reset(); if (transformer.Rule.Operation.YieldTokenBeforeOperation()) { transformedBuffer.Add(token); bufferAfterChecks = false; } else { bufferAfterChecks = transformer.Rule.Operation.YieldTokenAfterOperation(); } switch (transformer.Rule.Operation) { case Prepend: transformedBuffer.AddRange(transformer.Rule.Tokens); transformedBuffer.AddRange(transformer.Buffer); transformer.Buffer.Clear(); break; case ReplaceLast: case Append: case ReplaceAll: transformer.Buffer.Clear(); transformedBuffer.AddRange(transformer.Rule.Tokens); break; case None: default: break; } mod.Logger.Information( $"[{name}] Patch {transformer.Rule.Name} OK!"); patchOccurrences[transformer.Rule.Name] = patchOccurrences[transformer.Rule.Name] with { Occurred = patchOccurrences[transformer.Rule.Name].Occurred + 1 }; } } if (bufferAfterChecks) { transformedBuffer.Add(token); } else { bufferAfterChecks = true; } } stagingBuffer.Clear(); stagingBuffer.AddRange(transformedBuffer); transformedBuffer.Clear(); } foreach (var result in patchOccurrences.Where(result => result.Value.Occurred != result.Value.Expected)) { mod.Logger.Error( $"[{name}] Patch {result.Key} FAILED! Times expected={result.Value.Expected}, actual={result.Value.Occurred}"); } return stagingBuffer; } }