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.
This commit is contained in:
maj00r
2026-09-04 22:06:59 +02:00
parent 673cda57d7
commit b33d7697a0
13 changed files with 113 additions and 92 deletions

View File

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

View File

@@ -5,6 +5,7 @@ using StarterNG.Application.Abstractions;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Domain.Settings; using StarterNG.Domain.Settings;
using StarterNG.Infrastructure.Settings; using StarterNG.Infrastructure.Settings;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Application; namespace StarterNG.Application;
@@ -50,7 +51,7 @@ public sealed class SettingsStore
_paths = paths; _paths = paths;
_serializer = serializer; _serializer = serializer;
_executables = executables; _executables = executables;
_encoding = LegacyEncoding(); _encoding = LegacyText.CodePage1250;
} }
/// <summary>The settings the application is running with.</summary> /// <summary>The settings the application is running with.</summary>
@@ -172,20 +173,4 @@ public sealed class SettingsStore
public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem); public string ResolveExecutable(out ExeProblem problem) => _executables.Resolve(Settings, out problem);
/// <summary>
/// The .ini keeps its own code page. UTF-8 is a safe fallback if the code
/// page provider is unavailable.
/// </summary>
private static Encoding LegacyEncoding()
{
try
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(1250);
}
catch (Exception)
{
return Encoding.UTF8;
}
}
} }

View File

@@ -6,6 +6,7 @@ using StarterNG.Application.Abstractions;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Domain; using StarterNG.Domain;
using StarterNG.Domain.Settings; using StarterNG.Domain.Settings;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Application; namespace StarterNG.Application;
@@ -125,7 +126,7 @@ public sealed class StartSimulation
{ {
_files.WriteAllText(exportPath, _files.WriteAllText(exportPath,
scenery.BuildExportContent(_settings.Settings.IgnoreIrrelevantTrains, _random), scenery.BuildExportContent(_settings.Settings.IgnoreIrrelevantTrains, _random),
LegacyEncoding()); LegacyText.CodePage1250);
} }
catch (Exception ex) 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;
}
}
} }

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Classes; namespace StarterNG.Classes;
@@ -43,8 +44,7 @@ public sealed class KeyboardConfig
try try
{ {
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); return LegacyText.CodePage1250;
return Encoding.GetEncoding(1250);
} }
catch catch
{ {

View File

@@ -8,6 +8,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata; using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Classes; namespace StarterNG.Classes;
@@ -100,7 +101,7 @@ public static class PresetStore
int added = 0; int added = 0;
try try
{ {
string text = File.ReadAllText(path, Encoding.GetEncoding(1250)); string text = File.ReadAllText(path, LegacyText.CodePage1250);
var section = Regex.Matches(text, var section = Regex.Matches(text,
@"\[TRAINSET\d+\s*=\s*([^\]]*)\](.*?)(?=\[TRAINSET|\z)", @"\[TRAINSET\d+\s*=\s*([^\]]*)\](.*?)(?=\[TRAINSET|\z)",
RegexOptions.IgnoreCase | RegexOptions.Singleline); RegexOptions.IgnoreCase | RegexOptions.Singleline);

View File

@@ -169,11 +169,17 @@ public static class TrainsetDisplay
if (p != null) if (p != null)
massKg += p.Mass; massKg += p.Mass;
if (loadWeightKg != null && if (loadWeightKg != null && car.LoadCount > 0 && !string.IsNullOrEmpty(car.LoadType))
car.LoadCount > 0 && {
!string.IsNullOrEmpty(car.LoadType) && if (LoadAccepted(p, car.LoadType))
LoadAccepted(p, car.LoadType)) loadKg += (double)car.LoadCount * loadWeightKg(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); return (lengthM, massKg, loadKg);
} }

View File

@@ -6,6 +6,7 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Infrastructure; using StarterNG.Infrastructure;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Domain; namespace StarterNG.Domain;
@@ -40,7 +41,7 @@ public static class UDepot
string text; string text;
try try
{ {
text = File.ReadAllText(file, Encoding.GetEncoding(1250)); text = File.ReadAllText(file, LegacyText.CodePage1250);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -141,7 +142,7 @@ public static class UDepot
string? dir = Path.GetDirectoryName(path); string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(path, sb.ToString(), Encoding.GetEncoding(1250)); File.WriteAllText(path, sb.ToString(), LegacyText.CodePage1250);
return true; return true;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -0,0 +1,43 @@
using System;
using System.Text;
namespace StarterNG.Infrastructure.Adapters;
/// <summary>
/// The code page every file the simulator owns is written in: sceneries,
/// textures.txt, .fiz, timetables, eu07.ini.
/// </summary>
/// <remarks>
/// Code page 1250 is not built into .NET; it arrives with
/// <see cref="CodePagesEncodingProvider"/>, which has to be registered before the
/// first <c>GetEncoding</c> 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.
/// </remarks>
public static class LegacyText
{
/// <summary>The .scn / .fiz / .ini code page, or Latin-1 if it is unavailable.</summary>
public static Encoding CodePage1250 { get; } = Resolve();
/// <summary>
/// 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.
/// </summary>
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;
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using StarterNG.Application.Abstractions; using StarterNG.Application.Abstractions;
using StarterNG.Domain.Sceneries; using StarterNG.Domain.Sceneries;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Sceneries; namespace StarterNG.Infrastructure.Sceneries;
@@ -36,7 +37,7 @@ public sealed class SceneryRepository : ISceneryRepository
_paths = paths; _paths = paths;
_log = log; _log = log;
_parser = parser; _parser = parser;
_encoding = CodePage1250(); _encoding = LegacyText.CodePage1250;
} }
public IReadOnlyList<Scenery> LoadAll(IProgress<SceneryLoadProgress>? progress = null) public IReadOnlyList<Scenery> LoadAll(IProgress<SceneryLoadProgress>? progress = null)
@@ -83,19 +84,4 @@ public sealed class SceneryRepository : ISceneryRepository
_files.GetFiles(_paths.Scenery, "*.scn") _files.GetFiles(_paths.Scenery, "*.scn")
.Where(path => Path.GetFileName(path).FirstOrDefault() != ExcludedPrefix); .Where(path => Path.GetFileName(path).FirstOrDefault() != ExcludedPrefix);
/// <summary>
/// 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.
/// </summary>
private static Encoding CodePage1250()
{
try
{
return Encoding.GetEncoding(1250);
}
catch (Exception)
{
return Encoding.Latin1;
}
}
} }

View File

@@ -7,6 +7,7 @@ using System.Linq;
using System.Text; using System.Text;
using StarterNG.Application.Abstractions; using StarterNG.Application.Abstractions;
using StarterNG.Domain.Vehicles; using StarterNG.Domain.Vehicles;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Vehicles; namespace StarterNG.Infrastructure.Vehicles;
@@ -51,7 +52,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
_files = files; _files = files;
_paths = paths; _paths = paths;
_log = log; _log = log;
_encoding = LegacyEncoding(); _encoding = LegacyText.CodePage1250;
} }
public void Preload() => Index(); public void Preload() => Index();
@@ -270,15 +271,4 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
.ToList(); .ToList();
} }
private static Encoding LegacyEncoding()
{
try
{
return Encoding.GetEncoding(1250);
}
catch (Exception)
{
return Encoding.Latin1;
}
}
} }

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.Text; using System.Text;
using StarterNG.Application.Abstractions; using StarterNG.Application.Abstractions;
using StarterNG.Domain.Vehicles; using StarterNG.Domain.Vehicles;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Vehicles; namespace StarterNG.Infrastructure.Vehicles;
@@ -51,7 +52,7 @@ public sealed class LoadWeightsRepository
try try
{ {
return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyEncoding()))); return new LoadWeightsTable(ParsePairs(_files.ReadAllText(path, LegacyText.CodePage1250)));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -100,15 +101,4 @@ public sealed class LoadWeightsRepository
return weights; return weights;
} }
private static Encoding LegacyEncoding()
{
try
{
return Encoding.GetEncoding(1250);
}
catch (Exception)
{
return Encoding.Latin1;
}
}
} }

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Text; using System.Text;
using StarterNG.Application.Abstractions; using StarterNG.Application.Abstractions;
using StarterNG.Domain.Vehicles; using StarterNG.Domain.Vehicles;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Infrastructure.Vehicles; namespace StarterNG.Infrastructure.Vehicles;
@@ -28,7 +29,7 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository
_paths = paths; _paths = paths;
_log = log; _log = log;
_parser = parser; _parser = parser;
_encoding = LegacyEncoding(); _encoding = LegacyText.CodePage1250;
} }
public int Load(VehicleCatalog catalog) public int Load(VehicleCatalog catalog)
@@ -98,15 +99,4 @@ public sealed class TexturesTxtVehicleRepository : IVehicleRepository
return relative.Replace('\\', '/').TrimEnd('/') + "/"; return relative.Replace('\\', '/').TrimEnd('/') + "/";
} }
private static Encoding LegacyEncoding()
{
try
{
return Encoding.GetEncoding(1250);
}
catch (Exception)
{
return Encoding.Latin1;
}
}
} }

View File

@@ -20,6 +20,7 @@ using StarterNG.Domain;
using StarterNG.Application; using StarterNG.Application;
using StarterNG.Domain.Sceneries; using StarterNG.Domain.Sceneries;
using StarterNG.Presentation.Scenarios; using StarterNG.Presentation.Scenarios;
using StarterNG.Infrastructure.Adapters;
namespace StarterNG.Views; namespace StarterNG.Views;
@@ -708,7 +709,7 @@ public partial class Scenarios : UserControl
try try
{ {
timetableContent.Text = File.ReadAllText(path, Encoding.GetEncoding(1250)); timetableContent.Text = File.ReadAllText(path, LegacyText.CodePage1250);
UpdateTimetableTab(true); UpdateTimetableTab(true);
} }
catch catch