From b33d7697a01649527f216352e6698f2ccba32a1c Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 4 Sep 2026 22:06:59 +0200 Subject: [PATCH] Fix cp1250 decoding, log unsupported loads The repositories resolved GetEncoding(1250) in their constructors, before anything registered CodePagesEncodingProvider - reads fell back to Latin-1 and Polish characters came out as mojibake. --- StarterNG.Tests/LegacyTextTests.cs | 38 ++++++++++++++++ StarterNG/Application/SettingsStore.cs | 19 +------- StarterNG/Application/StartSimulation.cs | 14 +----- StarterNG/Classes/KeyboardConfig.cs | 4 +- StarterNG/Classes/PresetStore.cs | 3 +- StarterNG/Domain/TrainsetDisplay.cs | 16 ++++--- StarterNG/Domain/UDepot.cs | 5 ++- .../Infrastructure/Adapters/LegacyText.cs | 43 +++++++++++++++++++ .../Sceneries/SceneryRepository.cs | 18 +------- .../Vehicles/FizPhysicsRepository.cs | 14 +----- .../Vehicles/LoadWeightsRepository.cs | 14 +----- .../Vehicles/TexturesTxtVehicleRepository.cs | 14 +----- StarterNG/Views/Scenarios.axaml.cs | 3 +- 13 files changed, 113 insertions(+), 92 deletions(-) create mode 100644 StarterNG.Tests/LegacyTextTests.cs create mode 100644 StarterNG/Infrastructure/Adapters/LegacyText.cs diff --git a/StarterNG.Tests/LegacyTextTests.cs b/StarterNG.Tests/LegacyTextTests.cs new file mode 100644 index 0000000..9e48eee --- /dev/null +++ b/StarterNG.Tests/LegacyTextTests.cs @@ -0,0 +1,38 @@ +using StarterNG.Infrastructure.Adapters; + +namespace StarterNG.Tests; + +public class LegacyTextTests +{ + [Fact] + public void The_game_code_page_resolves_without_anyone_registering_it_first() + { + // Regression guard: when this falls back, every Polish character in a + // scenery description comes out as Latin-1 mojibake ("Skład" as "Sk³ad"). + Assert.False(LegacyText.IsFallback); + Assert.Equal(1250, LegacyText.CodePage1250.CodePage); + } + + [Fact] + public void Polish_characters_survive_a_round_trip_through_it() + { + const string text = "Skład wygaszony pod wjazdowym. Strzebiń - Woźniki Śl."; + + byte[] bytes = LegacyText.CodePage1250.GetBytes(text); + + Assert.Equal(text, LegacyText.CodePage1250.GetString(bytes)); + } + + [Fact] + public void A_scenery_written_in_the_game_code_page_reads_back_intact() + { + var files = new StarterNG.Tests.Fakes.InMemoryFileSystem() + .WithLegacyFile(StarterNG.Tests.Fakes.TestInstallation.At("scenery", "td.scn"), + "//$n Strzebiń - Woźniki Śl. '92\n//$d Skład wygaszony pod wjazdowym."); + + var scenery = Assert.Single(new StarterNG.Tests.Fakes.TestInstallation(files).Sceneries.LoadAll()); + + Assert.Equal("Strzebiń - Woźniki Śl. '92", scenery.Name); + Assert.Equal("Skład wygaszony pod wjazdowym.", scenery.Description); + } +} diff --git a/StarterNG/Application/SettingsStore.cs b/StarterNG/Application/SettingsStore.cs index 9edb3d0..bdd40c9 100644 --- a/StarterNG/Application/SettingsStore.cs +++ b/StarterNG/Application/SettingsStore.cs @@ -5,6 +5,7 @@ using StarterNG.Application.Abstractions; using StarterNG.Classes; using StarterNG.Domain.Settings; using StarterNG.Infrastructure.Settings; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Application; @@ -50,7 +51,7 @@ public sealed class SettingsStore _paths = paths; _serializer = serializer; _executables = executables; - _encoding = LegacyEncoding(); + _encoding = LegacyText.CodePage1250; } /// The settings the application is running with. @@ -172,20 +173,4 @@ public sealed class SettingsStore public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem); - /// - /// The .ini keeps its own code page. UTF-8 is a safe fallback if the code - /// page provider is unavailable. - /// - private static Encoding LegacyEncoding() - { - try - { - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.UTF8; - } - } } diff --git a/StarterNG/Application/StartSimulation.cs b/StarterNG/Application/StartSimulation.cs index fd28b53..a6b75dd 100644 --- a/StarterNG/Application/StartSimulation.cs +++ b/StarterNG/Application/StartSimulation.cs @@ -6,6 +6,7 @@ using StarterNG.Application.Abstractions; using StarterNG.Classes; using StarterNG.Domain; using StarterNG.Domain.Settings; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Application; @@ -125,7 +126,7 @@ public sealed class StartSimulation { _files.WriteAllText(exportPath, scenery.BuildExportContent(_settings.Settings.IgnoreIrrelevantTrains, _random), - LegacyEncoding()); + LegacyText.CodePage1250); } catch (Exception ex) { @@ -174,15 +175,4 @@ public sealed class StartSimulation }; } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Classes/KeyboardConfig.cs b/StarterNG/Classes/KeyboardConfig.cs index b3fd6cd..b717853 100644 --- a/StarterNG/Classes/KeyboardConfig.cs +++ b/StarterNG/Classes/KeyboardConfig.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Classes; @@ -43,8 +44,7 @@ public sealed class KeyboardConfig try { - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - return Encoding.GetEncoding(1250); + return LegacyText.CodePage1250; } catch { diff --git a/StarterNG/Classes/PresetStore.cs b/StarterNG/Classes/PresetStore.cs index c024e57..b03a748 100644 --- a/StarterNG/Classes/PresetStore.cs +++ b/StarterNG/Classes/PresetStore.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Classes; @@ -100,7 +101,7 @@ public static class PresetStore int added = 0; try { - string text = File.ReadAllText(path, Encoding.GetEncoding(1250)); + string text = File.ReadAllText(path, LegacyText.CodePage1250); var section = Regex.Matches(text, @"\[TRAINSET\d+\s*=\s*([^\]]*)\](.*?)(?=\[TRAINSET|\z)", RegexOptions.IgnoreCase | RegexOptions.Singleline); diff --git a/StarterNG/Domain/TrainsetDisplay.cs b/StarterNG/Domain/TrainsetDisplay.cs index 040add4..10037af 100644 --- a/StarterNG/Domain/TrainsetDisplay.cs +++ b/StarterNG/Domain/TrainsetDisplay.cs @@ -169,11 +169,17 @@ public static class TrainsetDisplay if (p != null) massKg += p.Mass; - if (loadWeightKg != null && - car.LoadCount > 0 && - !string.IsNullOrEmpty(car.LoadType) && - LoadAccepted(p, car.LoadType)) - loadKg += (double)car.LoadCount * loadWeightKg(car.LoadType); + if (loadWeightKg != null && car.LoadCount > 0 && !string.IsNullOrEmpty(car.LoadType)) + { + if (LoadAccepted(p, car.LoadType)) + loadKg += (double)car.LoadCount * loadWeightKg(car.LoadType); + else + // The scenery loads something this vehicle's .fiz does not + // accept. It is left out of the gross mass, and saying so beats + // leaving the reading quietly short. + AppServices.Current.Log.Log( + $"{car.Name}: nieobslugiwany ladunek \"{car.LoadType}\""); + } } return (lengthM, massKg, loadKg); } diff --git a/StarterNG/Domain/UDepot.cs b/StarterNG/Domain/UDepot.cs index 25beae8..36ee242 100644 --- a/StarterNG/Domain/UDepot.cs +++ b/StarterNG/Domain/UDepot.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using StarterNG.Classes; using StarterNG.Infrastructure; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Domain; @@ -40,7 +41,7 @@ public static class UDepot string text; try { - text = File.ReadAllText(file, Encoding.GetEncoding(1250)); + text = File.ReadAllText(file, LegacyText.CodePage1250); } catch (Exception ex) { @@ -141,7 +142,7 @@ public static class UDepot string? dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); - File.WriteAllText(path, sb.ToString(), Encoding.GetEncoding(1250)); + File.WriteAllText(path, sb.ToString(), LegacyText.CodePage1250); return true; } catch (Exception ex) diff --git a/StarterNG/Infrastructure/Adapters/LegacyText.cs b/StarterNG/Infrastructure/Adapters/LegacyText.cs new file mode 100644 index 0000000..aa48b34 --- /dev/null +++ b/StarterNG/Infrastructure/Adapters/LegacyText.cs @@ -0,0 +1,43 @@ +using System; +using System.Text; + +namespace StarterNG.Infrastructure.Adapters; + +/// +/// The code page every file the simulator owns is written in: sceneries, +/// textures.txt, .fiz, timetables, eu07.ini. +/// +/// +/// Code page 1250 is not built into .NET; it arrives with +/// , which has to be registered before the +/// first GetEncoding call. Doing that registration at each call site is how +/// Polish characters end up as Latin-1 mojibake ("Skład" as "Sk³ad"): whichever +/// site runs first wins, and the composition root builds its repositories before +/// any of them. One property, resolved once, removes the ordering question. +/// +public static class LegacyText +{ + /// The .scn / .fiz / .ini code page, or Latin-1 if it is unavailable. + public static Encoding CodePage1250 { get; } = Resolve(); + + /// + /// True when the code page could not be loaded and text is being read as + /// Latin-1 instead. Polish characters will be wrong; the startup check + /// reports it rather than leaving it to be discovered on screen. + /// + public static bool IsFallback { get; private set; } + + private static Encoding Resolve() + { + try + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + return Encoding.GetEncoding(1250); + } + catch (Exception) + { + IsFallback = true; + return Encoding.Latin1; + } + } +} diff --git a/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs b/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs index afcb536..e8308a2 100644 --- a/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs +++ b/StarterNG/Infrastructure/Sceneries/SceneryRepository.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using StarterNG.Application.Abstractions; using StarterNG.Domain.Sceneries; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Sceneries; @@ -36,7 +37,7 @@ public sealed class SceneryRepository : ISceneryRepository _paths = paths; _log = log; _parser = parser; - _encoding = CodePage1250(); + _encoding = LegacyText.CodePage1250; } public IReadOnlyList LoadAll(IProgress? progress = null) @@ -83,19 +84,4 @@ public sealed class SceneryRepository : ISceneryRepository _files.GetFiles(_paths.Scenery, "*.scn") .Where(path => Path.GetFileName(path).FirstOrDefault() != ExcludedPrefix); - /// - /// The .scn dialect predates Unicode. The provider is registered by the entry - /// point; fall back to Latin-1 if that ever fails so loading still works. - /// - private static Encoding CodePage1250() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs b/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs index 1856982..b530a59 100644 --- a/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/FizPhysicsRepository.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using StarterNG.Application.Abstractions; using StarterNG.Domain.Vehicles; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; @@ -51,7 +52,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository _files = files; _paths = paths; _log = log; - _encoding = LegacyEncoding(); + _encoding = LegacyText.CodePage1250; } public void Preload() => Index(); @@ -270,15 +271,4 @@ public sealed class FizPhysicsRepository : IPhysicsRepository .ToList(); } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs index 486c991..8639771 100644 --- a/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/LoadWeightsRepository.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using StarterNG.Application.Abstractions; using StarterNG.Domain.Vehicles; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; @@ -51,7 +52,7 @@ public sealed class LoadWeightsRepository try { - return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyEncoding()))); + return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyText.CodePage1250))); } catch (Exception ex) { @@ -100,15 +101,4 @@ public sealed class LoadWeightsRepository return weights; } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs b/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs index 6c959ab..d2f709c 100644 --- a/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs +++ b/StarterNG/Infrastructure/Vehicles/TexturesTxtVehicleRepository.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text; using StarterNG.Application.Abstractions; using StarterNG.Domain.Vehicles; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Infrastructure.Vehicles; @@ -28,7 +29,7 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository _paths = paths; _log = log; _parser = parser; - _encoding = LegacyEncoding(); + _encoding = LegacyText.CodePage1250; } public int Load(VehicleCatalog catalog) @@ -98,15 +99,4 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository return relative.Replace('\\', '/').TrimEnd('/') + "/"; } - private static Encoding LegacyEncoding() - { - try - { - return Encoding.GetEncoding(1250); - } - catch (Exception) - { - return Encoding.Latin1; - } - } } diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs index 3339d08..a55b984 100644 --- a/StarterNG/Views/Scenarios.axaml.cs +++ b/StarterNG/Views/Scenarios.axaml.cs @@ -20,6 +20,7 @@ using StarterNG.Domain; using StarterNG.Application; using StarterNG.Domain.Sceneries; using StarterNG.Presentation.Scenarios; +using StarterNG.Infrastructure.Adapters; namespace StarterNG.Views; @@ -708,7 +709,7 @@ public partial class Scenarios : UserControl try { - timetableContent.Text = File.ReadAllText(path, Encoding.GetEncoding(1250)); + timetableContent.Text = File.ReadAllText(path, LegacyText.CodePage1250); UpdateTimetableTab(true); } catch