using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using StarterNG.Classes; namespace StarterNG.Domain.Sceneries; /// /// A trainset written back into its own scenery file. Only the vehicles that /// changed are the starter's to rewrite: everything else the author put there - /// the header as typed, the description, comments, an assignments block, the other /// vehicles as they are spelled - stays as it was, and the rest of the file is not /// touched at all. /// public static class SceneryTrainsetText { private static readonly Regex LoneLineFeed = new(@"(? /// The trainset as the file should now hold it, in the file's own line ends. /// Ends at endtrainset, as the parser's match of the old one did. /// /// /// What stands between the vehicles stays in its place among those that stay. /// A comment inside a vehicle's own node goes when that node is rewritten or /// the vehicle removed: there is no longer a place for it. /// public static string Compose(Trainset trainset, string newline) { string raw = trainset.RawEntry; var saved = new Trainset(raw); // Not read to its end: what stands there is not known, so it all stays. if (!saved.Parsed) return raw; var (speed, vehicles, end) = saved.Layout; // Line ends of both kinds are left as they are: there is no one kind to give them. bool mixed = raw.Contains("\r\n", StringComparison.Ordinal) && LoneLineFeed.IsMatch(raw); string Own(string text) => mixed ? text : Normalize(text).Replace("\n", newline); int first = vehicles.Count > 0 ? vehicles[0].Start : end; string head = raw[..first]; // The speed is the one header field the depot changes - the ready switch. // Rewritten only when it differs, so "0.0" stays "0.0". if (saved.Velocity != trainset.Velocity && speed is { } field) head = raw[..field.Index] + trainset.Velocity.ToString(CultureInfo.InvariantCulture) + raw[field.End..first]; string[] was = saved.Vehicles.Select(v => v.ToTrainsetNode()).ToArray(); string[] now = trainset.Vehicles.Select(v => v.ToTrainsetNode()).ToArray(); string ending = end < raw.Length ? raw[end..] : "endtrainset"; if (was.SequenceEqual(now)) return Own(head + raw[first..end] + ending); string Written(int i) => raw[vehicles[i].Start..vehicles[i].End]; string After(int i) => raw[vehicles[i].End..(i + 1 < vehicles.Count ? vehicles[i + 1].Start : end)]; var sb = new StringBuilder(Own(head)); void AtLineStart() { if (sb.Length > 0 && sb[^1] != '\n') sb.Append(newline); } // One moved here is still written as it was. void Rewrite(int j) { AtLineStart(); int written = Array.IndexOf(was, now[j]); sb.Append(written >= 0 ? Own(Written(written)) : now[j].TrimEnd('\n')); } int[,] staying = Staying(was, now); for (int i = 0, j = 0; i < was.Length || j < now.Length;) { bool both = i < was.Length && j < now.Length; if (both && was[i] == now[j]) { sb.Append(Own(Written(i) + After(i))); i++; j++; } else if (both && staying[i + 1, j + 1] == staying[i, j]) { // In the place of the one there before, so what followed that one stays. Rewrite(j++); sb.Append(Own(After(i++))); } else if (j < now.Length && (i == was.Length || staying[i, j + 1] == staying[i, j])) { Rewrite(j++); sb.Append(newline); } else { // Gone, the end of its line with it; what followed it stays. string after = After(i++); if (!string.IsNullOrWhiteSpace(after)) { AtLineStart(); sb.Append(Own(RestOfLine.Replace(after, "", 1))); } } } AtLineStart(); return sb.Append(Own(ending)).ToString(); } /// /// How many vehicles can stay in the order they are in, of those from each place on /// in the old list and in the new one: the longest common run of the two. /// private static int[,] Staying(string[] was, string[] now) { var staying = new int[was.Length + 1, now.Length + 1]; for (int i = was.Length - 1; i >= 0; i--) for (int j = now.Length - 1; j >= 0; j--) staying[i, j] = was[i] == now[j] ? staying[i + 1, j + 1] + 1 : Math.Max(staying[i + 1, j], staying[i, j + 1]); return staying; } /// /// The file with the trainset's old text swapped for , /// or null when that text cannot be told apart in the file: missing, or found /// more often than the scenery has trainsets written that way. /// public static string? Replace(string fileText, Trainset trainset, IReadOnlyList sceneryTrainsets, string entry) { var twins = sceneryTrainsets.Where(t => t.RawEntry == trainset.RawEntry).ToList(); int ordinal = twins.FindIndex(t => ReferenceEquals(t, trainset)); if (ordinal < 0 || trainset.RawEntry.Length == 0) return null; var found = new List(); for (int at = fileText.IndexOf(trainset.RawEntry, StringComparison.Ordinal); at >= 0; at = fileText.IndexOf(trainset.RawEntry, at + trainset.RawEntry.Length, StringComparison.Ordinal)) found.Add(at); if (found.Count != twins.Count) return null; int start = found[ordinal]; return fileText[..start] + entry + fileText[(start + trainset.RawEntry.Length)..]; } /// /// Whether the trainset now differs from what its file holds, compared the way /// both would be written so a different spelling of the same number is no change. /// public static bool IsModified(Trainset trainset) => trainset.Parsed && Signature(trainset) != SavedSignature(trainset.RawEntry); /// /// By the text a trainset holds, for as long as one holds it: a scenery read again /// brings texts of its own, and those of the one it replaces go with it. /// private static readonly ConditionalWeakTable SavedSignatures = new(); private static string SavedSignature(string raw) => SavedSignatures.GetValue(raw, text => Signature(new Trainset(text))); private static string Signature(Trainset trainset) => trainset.Velocity.ToString(CultureInfo.InvariantCulture) + "\n" + Dynamic.ToTrainsetNodes(trainset.Vehicles); private static string Normalize(string text) => text.Replace("\r\n", "\n"); }