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.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;
}
/// <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);
/// <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.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;
}
}
}

View File

@@ -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
{

View File

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

View File

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

View File

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

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 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<Scenery> LoadAll(IProgress<SceneryLoadProgress>? progress = null)
@@ -83,19 +84,4 @@ public sealed class SceneryRepository : ISceneryRepository
_files.GetFiles(_paths.Scenery, "*.scn")
.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 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;
}
}
}

View File

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

View File

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

View File

@@ -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