wider migration from old starter

This commit is contained in:
maj00r
2026-08-30 22:29:45 +02:00
parent c7853f6588
commit 563a937eb9
44 changed files with 6076 additions and 1457 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
/StarterNG/obj
/.idea
/.vs
starter/

48
README.md Normal file
View File

@@ -0,0 +1,48 @@
# StarterNG
Launcher for the MaSzyna train simulator, written in C# with Avalonia.
## Building a native (NativeAOT) build
The project publishes as a self-contained NativeAOT binary — no .NET runtime is
needed on the target machine. The commands below are the same ones CI runs, see
[`.github/workflows/dotnet.yml`](.github/workflows/dotnet.yml).
Requirements:
- .NET SDK 9
- Linux: a native toolchain and zlib headers — `sudo apt install clang zlib1g-dev`
(Debian/Ubuntu) or `sudo dnf install clang zlib-devel` (Fedora)
- Windows: the Visual Studio C++ build tools (Desktop development with C++)
Restore for the target runtime first, then publish:
```sh
dotnet restore StarterNG/StarterNG.csproj -r linux-x64
dotnet publish StarterNG/StarterNG.csproj \
-c Release -r linux-x64 --self-contained true --no-restore \
-p:PublishAot=true -p:PublishTrimmed=true \
-o publish/linux-x64
```
For a Windows build swap both `linux-x64` occurrences for `win-x64` (and the
line continuations for PowerShell backticks). NativeAOT does not cross-compile,
so the Windows package has to be built on Windows and the Linux one on Linux —
that is why CI uses a `windows-2022` and an `ubuntu-22.04` runner.
The output directory is the whole distributable: the `Starter` executable, the
Skia/HarfBuzz native libraries and `startercfg/` with the translations and the
default key bindings. The `.dbg` (or `.pdb`) file next to it is debug symbols and
can be dropped from a release. Trim/AOT analysis warnings during the publish are
expected and do not fail the build.
CI builds both runtimes on every push and pull request to `main` and `staging`,
and on manual dispatch. Pushes additionally replace a rolling pre-release with
the packaged artifacts — tagged `latest` for `main` and `staging` for `staging`.
## Development build
```sh
dotnet build StarterNG/StarterNG.csproj
```

View File

@@ -5,18 +5,12 @@
x:Class="StarterNG.App"
x:CompileBindings="True"
RequestedThemeVariant="Dark">
<!-- Dark variant matches the original Starter's "Carbon" VCL style. -->
<Application.Styles>
<FluentTheme />
<materialIcons:MaterialIconStyles />
<StyleInclude Source="avares://Starter/Styles/Carbon.axaml" />
<!-- Extending the client area (MainWindow) makes Avalonia 12 draw an extra
full-screen toggle in the window chrome. The chrome lives in its own host
(not the window's visual tree), so it is hidden here, globally, by the
decoration role Avalonia tags it with. Standard minimise / maximise /
close are left untouched. -->
<Style Selector="Button[(chrome|WindowDecorationProperties.ElementRole)=FullScreenButton]">
<Setter Property="IsVisible" Value="False" />
</Style>
@@ -24,9 +18,6 @@
<Application.Resources>
<PathGeometry x:Key="DiscordLogo" Figures="M 81.15 0 c -1.2376 2.1973 -2.3489 4.4704 -3.3591 6.794 c -9.5975 -1.4396 -19.3718 -1.4396 -28.9945 0 c -0.985 -2.3236 -2.1216 -4.5967 -3.3591 -6.794 c -9.0166 1.5407 -17.8059 4.2431 -26.1405 8.0568 C 2.779 32.5304 -1.6914 56.3725 0.5312 79.8863 c 9.6732 7.1476 20.5083 12.603 32.0505 16.0884 c 2.6014 -3.4854 4.8998 -7.1981 6.8698 -11.0623 c -3.738 -1.3891 -7.3497 -3.1318 -10.8098 -5.1523 c 0.9092 -0.6567 1.7932 -1.3386 2.6519 -1.9953 c 20.281 9.547 43.7696 9.547 64.0758 0 c 0.8587 0.7072 1.7427 1.3891 2.6519 1.9953 c -3.4601 2.0457 -7.0718 3.7632 -10.835 5.1776 c 1.97 3.8642 4.2683 7.5769 6.8698 11.0623 c 11.5419 -3.4854 22.3769 -8.9156 32.0509 -16.0631 c 2.626 -27.2771 -4.496 -50.9172 -18.817 -71.8548 C 98.9811 4.2684 90.1918 1.5659 81.1752 0.0505 l -0.0252 -0.0505 Z M 42.2802 65.4144 c -6.2383 0 -11.4159 -5.6575 -11.4159 -12.6535 s 4.9755 -12.6788 11.3907 -12.6788 s 11.5169 5.708 11.4159 12.6788 c -0.101 6.9708 -5.026 12.6535 -11.3907 12.6535 Z M 84.3576 65.4144 c -6.2637 0 -11.3907 -5.6575 -11.3907 -12.6535 s 4.9755 -12.6788 11.3907 -12.6788 s 11.4917 5.708 11.3906 12.6788 c -0.101 6.9708 -5.026 12.6535 -11.3906 12.6535 Z" FillRule="NonZero"/>
<!-- Override the system accent (the OS accent can be red) so every
accent-driven element - list/tree selection, checkboxes,
sliders - uses the MaSzyna green instead. -->
<Color x:Key="SystemAccentColor">#177f00</Color>
<Color x:Key="SystemAccentColorDark1">#136B00</Color>
<Color x:Key="SystemAccentColorDark2">#0F5600</Color>
@@ -39,5 +30,13 @@
<SolidColorBrush x:Key="AccentFillColorTertiaryBrush" Color="#136B00" />
<SolidColorBrush x:Key="SystemControlHighlightListAccentLowBrush" Color="#177f00" />
<SolidColorBrush x:Key="SystemControlHighlightListAccentMediumBrush" Color="#1E9000" />
<GridLength x:Key="SliderPreContentMargin">6</GridLength>
<GridLength x:Key="SliderPostContentMargin">6</GridLength>
<x:Double x:Key="SliderHorizontalHeight">24</x:Double>
<x:Double x:Key="SliderHorizontalThumbWidth">12</x:Double>
<x:Double x:Key="SliderHorizontalThumbHeight">12</x:Double>
<CornerRadius x:Key="SliderThumbCornerRadius">2</CornerRadius>
<x:Double x:Key="SliderTrackThemeHeight">4</x:Double>
</Application.Resources>
</Application>

View File

@@ -9,6 +9,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using StarterNG.Classes;
using StarterNG.Infrastructure;
using StarterNG.Services;
namespace StarterNG;
@@ -45,6 +46,30 @@ public partial class App : Application
var resolvedName = Loc.Load(langNameOrCode);
Settings.Instance.Language = resolvedName;
ApplyCulture(Loc.CurrentLangCode);
}
private static void ApplyCulture(string langCode)
{
CultureInfo culture;
try
{
culture = langCode switch
{
"" or "en" => CultureInfo.InvariantCulture,
"cz" => CultureInfo.GetCultureInfo("cs"),
_ => CultureInfo.GetCultureInfo(langCode)
};
}
catch (CultureNotFoundException)
{
culture = CultureInfo.InvariantCulture;
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
public override void OnFrameworkInitializationCompleted()
@@ -56,13 +81,23 @@ public partial class App : Application
desktop.ShutdownRequested += (_, _) =>
{
Settings.Instance.CaptureAndSave();
try { Settings.Instance.CaptureAndSave(); }
catch (Exception ex) { Diagnostics.Log("Settings save", ex); }
Settings.Instance.DumpMissingVehicleLog();
if (KeyboardConfig.Instance.Dirty)
KeyboardConfig.Instance.Save();
try
{
if (KeyboardConfig.Instance.Dirty)
KeyboardConfig.Instance.Save();
}
catch (Exception ex) { Diagnostics.Log("Keyboard config save", ex); }
Diagnostics.Flush();
};
SplashWindow? splash = null;
bool mainReady = false;
var sw = System.Diagnostics.Stopwatch.StartNew();
Task.Run(async () =>
@@ -71,6 +106,8 @@ public partial class App : Application
{
Dispatcher.UIThread.Post(() =>
{
if (mainReady)
return;
if (sw.ElapsedMilliseconds > 300 && splash == null)
{
@@ -83,14 +120,47 @@ public partial class App : Application
});
GameData.Instance.Load(progress);
}).ContinueWith(_ =>
}).ContinueWith(load =>
{
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Post(async () =>
{
var main = new MainWindow();
MainWindow main;
try
{
main = new MainWindow();
}
catch (Exception ex)
{
Diagnostics.Log("Main window", ex);
Diagnostics.Flush();
Console.Error.WriteLine(ex);
mainReady = true;
splash?.Close();
desktop.Shutdown(1);
return;
}
mainReady = true;
desktop.MainWindow = main;
main.Show();
splash?.Close();
splash = null;
if (load.IsFaulted && load.Exception is { } error)
{
var inner = error.GetBaseException();
Diagnostics.Log("Game data load", inner);
await Diagnostics.ReportAsync(
$"{Loc["FaultLoadData"]}{Environment.NewLine}{Environment.NewLine}{Loc["FaultDetail"]} {inner.Message}");
}
var faults = Diagnostics.CheckInstallation();
if (faults.Count > 0)
{
await Diagnostics.ReportAsync(
string.Join(Environment.NewLine, faults) + Environment.NewLine + Environment.NewLine +
Loc["FaultBadInstall"]);
}
});
});
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -46,6 +46,17 @@ public sealed class GameData
catch { }
done++;
}
try
{
int legacy = Vehicles.ImportLegacy();
if (legacy > 0)
Infrastructure.Diagnostics.Log($"textures.txt: wczytano {legacy} tekstur");
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log("textures.txt import", ex);
}
Vehicles.EndLoad();
VehicleDatabase.PreloadMiniIndex(miniDir);

View File

@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace StarterNG.Classes;
public static class LegacyTextureDatabase
{
public static List<string> EnumerateFiles(string dynamicRoot = "dynamic")
{
var files = new List<string>();
if (!Directory.Exists(dynamicRoot))
return files;
foreach (string group in Directory.EnumerateDirectories(dynamicRoot))
foreach (string sub in Directory.EnumerateDirectories(group))
{
string path = Path.Combine(sub, "textures.txt");
if (File.Exists(path))
files.Add(path);
}
return files;
}
public static VehicleEntry? Parse(string path, string dynamicRoot = "dynamic")
{
string[] lines;
try
{
lines = File.ReadAllLines(path, Encoding.GetEncoding(1250));
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log($"textures.txt {path}", ex);
return null;
}
string dir = RelativeDirectory(path, dynamicRoot);
var entry = new VehicleEntry { Uuid = "legacy:" + dir };
bool archived = false;
string categorySign = "*";
int setSize = 0;
var pending = new List<string>();
foreach (string raw in lines)
{
string line = raw.Trim();
if (line.Length == 0) continue;
char c = line[0];
if (c is '#' or '@' or '*') continue;
if (line.StartsWith("$a", StringComparison.OrdinalIgnoreCase))
{
archived = true;
continue;
}
if (c == '^')
{
FlushSet(entry, pending, setSize);
setSize = line.Length > 1 && char.IsDigit(line[1]) ? line[1] - '0' : 0;
continue;
}
if (line.StartsWith("!=", StringComparison.Ordinal))
{
categorySign = line.Length > 2 ? line[2].ToString() : "*";
continue;
}
if (line.IndexOf('=') < 0) continue;
if (line.StartsWith("//", StringComparison.Ordinal)) continue;
var texture = ParseEntryLine(line, dir, categorySign, archived, entry);
if (texture is null) continue;
entry.Textures.Add(texture);
if (setSize > 1)
{
pending.Add(texture.Uuid!);
if (pending.Count == setSize)
FlushSet(entry, pending, setSize);
}
}
FlushSet(entry, pending, setSize);
return entry.Textures.Count > 0 ? entry : null;
}
private static void FlushSet(VehicleEntry entry, List<string> pending, int size)
{
if (pending.Count > 1)
entry.Sets.Add(new VehicleSet
{
Uuid = "legacy-set:" + pending[0],
Mode = "sequence",
Count = size > 0 ? size : pending.Count,
TextureRefs = new List<string>(pending)
});
pending.Clear();
}
private static VehicleTexture? ParseEntryLine(string line, string dir,
string categorySign, bool archived, VehicleEntry entry)
{
string desc = "";
int comment = line.IndexOf("//", StringComparison.Ordinal);
if (comment >= 0)
{
desc = line[(comment + 2)..].Trim();
line = line[..comment].Trim();
}
var parts = line.Split('=');
if (parts.Length < 2) return null;
string skin = StripExtension(parts[0].Trim());
if (skin.Length == 0) return null;
var specs = new List<(string Model, string Mini, string MiniD)>();
for (int i = 1; i < parts.Length; i++)
{
var f = parts[i].Split(',');
if (f.Length < 2) continue;
specs.Add((f[0].Trim(), f[1].Trim(), f.Length > 2 ? f[2].Trim() : ""));
}
if (specs.Count == 0) return null;
var first = specs[0];
string groupId = $"legacy:{dir}:{first.Mini}:{categorySign}:{(archived ? 1 : 0)}";
if (!entry.Groups.Exists(g => g.Id == groupId))
entry.Groups.Add(new VehicleGroup
{
Id = groupId,
Category = categorySign,
Mini = first.Mini,
Archived = archived,
Implicit = true
});
var texture = new VehicleTexture
{
Uuid = $"legacy:{dir}{skin}",
Directory = dir,
Skinfile = skin,
Model = first.Model,
Group = groupId,
MiniRef = first.Mini,
TextureMini = string.IsNullOrEmpty(first.MiniD) ? null : first.MiniD,
Meta = ParseDescription(desc)
};
for (int i = 1; i < specs.Count; i++)
texture.Aliases.Add(new VehicleAlias
{
Model = specs[i].Model,
MiniRef = specs[i].Mini,
TextureMini = string.IsNullOrEmpty(specs[i].MiniD) ? null : specs[i].MiniD
});
return texture;
}
private static VehicleMeta? ParseDescription(string desc)
{
if (string.IsNullOrWhiteSpace(desc)) return null;
var f = desc.Replace('_', ' ').Split(',');
string At(int i) => i < f.Length ? f[i].Trim() : "";
return new VehicleMeta
{
Raw = desc,
Version = At(0),
Vehicle = At(1),
Operator = At(2),
Depot = At(3),
RevisionDate = At(4),
RevisionPlace = At(5),
TextureAuthor = At(6),
PhotoAuthor = At(7)
};
}
private static string StripExtension(string name)
{
int dot = name.LastIndexOf('.');
return dot > 0 ? name[..dot] : name;
}
private static string RelativeDirectory(string texturesPath, string dynamicRoot)
{
string dir = Path.GetDirectoryName(texturesPath) ?? "";
string full = Path.GetFullPath(dir);
string root = Path.GetFullPath(dynamicRoot);
string rel = full.StartsWith(root, StringComparison.OrdinalIgnoreCase)
? full[root.Length..].TrimStart(Path.DirectorySeparatorChar, '/')
: dir;
return rel.Replace('\\', '/').TrimEnd('/') + "/";
}
}

View File

@@ -24,6 +24,8 @@ public sealed class Physics
public static void PreloadIndex(string dynamicRoot = "dynamic") => FizIndex(dynamicRoot);
public static int IndexedCount => _fizIndex?.Count ?? 0;
private static Dictionary<string, string> FizIndex(string dynamicRoot)
{
if (_fizIndex != null)

View File

@@ -130,7 +130,10 @@ public static class PresetStore
added++;
}
}
catch { }
catch (Exception ex)
{
Infrastructure.Diagnostics.Log($"Import {path}", ex);
}
return added;
}

View File

@@ -49,7 +49,11 @@ public class Scenery
public double Temperature = 15;
public int FogStart = 10;
public int FogEnd = 2000;
public const int FogMin = 10;
public const int FogMax = 10000;
public double Overcast = 0;
public bool OvercastRandom;
public bool HasWeather;
@@ -61,6 +65,7 @@ public class Scenery
private double _origTemperature = 15;
private int _origFogEnd = 2000;
private double _origOvercast;
private bool _origOvercastRandom;
private readonly string _template;
@@ -68,8 +73,6 @@ public class Scenery
{
get
{
if (!string.IsNullOrWhiteSpace(Name))
return SceneryI18n.T(Name);
return System.IO.Path.GetFileNameWithoutExtension(Path);
}
}
@@ -149,7 +152,7 @@ public class Scenery
RangeMin = float.Parse(m.Groups[2].Value, inv),
Name = name,
DataFolder = m.Groups[4].Value,
SkinFile = m.Groups[5].Value,
SkinFile = Dynamic.StripSkinExtension(m.Groups[5].Value),
MmdFile = m.Groups[6].Value,
PathName = m.Groups[7].Value,
Offset = float.Parse(m.Groups[8].Value, inv),
@@ -356,8 +359,8 @@ public class Scenery
int.TryParse(nums[3], out int fogStart) &&
int.TryParse(nums[4], out int fogEnd))
{
FogStart = Math.Clamp(fogStart, 10, 2500);
fogEnd = Math.Clamp(fogEnd, FogStart, 2500);
FogStart = Math.Clamp(fogStart, FogMin, FogMax);
fogEnd = Math.Clamp(fogEnd, FogStart, FogMax);
FogEnd = fogEnd > FogStart
? Random.Shared.Next(FogStart, fogEnd + 1)
: fogEnd;
@@ -378,8 +381,17 @@ public class Scenery
_origTemperature = Temperature;
_origFogEnd = FogEnd;
_origOvercast = Overcast;
_origOvercastRandom = OvercastRandom;
}
public bool WeatherChanged =>
Day != _origDay ||
ScenarioTimeOverride != _origOverride ||
Math.Abs(Temperature - _origTemperature) > 0.001 ||
FogEnd != _origFogEnd ||
OvercastRandom != _origOvercastRandom ||
(!OvercastRandom && Math.Abs(Overcast - _origOvercast) > 0.001);
public void RestoreWeather()
{
Time = _origTime;
@@ -388,15 +400,24 @@ public class Scenery
Temperature = _origTemperature;
FogEnd = _origFogEnd;
Overcast = _origOvercast;
OvercastRandom = _origOvercastRandom;
WeatherDirty = true;
}
private static readonly double[] OvercastPresets = { -1.5, 0, 0.3, 0.7, 1, 1.1, 1.5 };
private double EffectiveOvercast() =>
OvercastRandom ? OvercastPresets[Random.Shared.Next(OvercastPresets.Length)] : Overcast;
private string RewriteWeather(string text)
{
try
{
string s = text;
string carried = CarriedConfigLines(s);
s = Regex.Replace(s, @"(?is)\bconfig\b.*?\bendconfig\b", " ");
s = Regex.Replace(s, @"(?is)\btime\b\s+\d[^\r\n]*?\bendtime\b", " ");
s = Regex.Replace(s, @"(?is)\batmo\b.*?\bendatmo\b", " ");
@@ -410,10 +431,11 @@ public class Scenery
"config\r\n" +
$"movelight {Day}\r\n" +
$"scenario.weather.temperature {Temperature.ToString(inv)}\r\n" +
$"scenario.time.override {ScenarioTimeOverride}\r\n" +
$"scenario.time.override {DelphiHourMinute(ScenarioTimeOverride)}\r\n" +
carried +
"endconfig\r\n" +
$"atmo 0 0 0 {FogEnd} {FogEnd} 0 0 0 {Overcast.ToString(inv)} endatmo\r\n" +
$"time {Time} 0 0 endtime\r\n";
$"atmo 0 0 0 {FogEnd} {FogEnd} 0 0 0 {EffectiveOvercast().ToString(inv)} endatmo\r\n" +
$"time {DelphiHourMinute(Time)} 0 0 endtime\r\n";
return config + s;
}
@@ -423,6 +445,36 @@ public class Scenery
}
}
private static string DelphiHourMinute(string hhmm)
{
int colon = hhmm.IndexOf(':');
if (colon <= 0) return hhmm;
return hhmm[..colon].TrimStart('0') is { Length: > 0 } h
? h + hhmm[colon..]
: "0" + hhmm[colon..];
}
private static readonly Regex ManagedConfigLine = new(
@"^\s*(movelight|scenario\.weather\.temperature|scenario\.time\.override)\b",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static string CarriedConfigLines(string text)
{
var block = Regex.Match(text, @"(?is)\bconfig\b(.*?)\bendconfig\b");
if (!block.Success)
return "";
var sb = new StringBuilder();
foreach (string raw in block.Groups[1].Value.Split('\n'))
{
string line = raw.TrimEnd('\r').Trim();
if (line.Length == 0 || ManagedConfigLine.IsMatch(line))
continue;
sb.Append(line).Append("\r\n");
}
return sb.ToString();
}
private static string MatchAllDirectives(string content, string letter)
{
var matches = Regex.Matches(

View File

@@ -95,6 +95,11 @@ public sealed class Settings
public int VehiclesVolume = 100;
public int PositionalVolume = 100;
public int AmbientVolume = 100;
public int VolumePaused = 100;
public bool SkipPipeline;
public string PyScreenPriority = "normal";
public bool DebugLogVisible;
private int _debugLogExtraBits;
public bool CompressTextures = true;
public bool ScaleSpeculars = true;
@@ -130,6 +135,10 @@ public sealed class Settings
private string _savePath = string.Empty;
private DateTime _configAgeUtc = DateTime.MinValue;
public string LoadedFrom { get; private set; } = string.Empty;
public string SavePath => _savePath;
public static string UserConfigPath()
{
if (OperatingSystem.IsWindows())
@@ -289,6 +298,7 @@ public sealed class Settings
{
_savePath = UserConfigPath();
string source = File.Exists(_savePath) ? _savePath : DefaultConfigPath();
LoadedFrom = source;
LoadFrom(source);
}
@@ -394,11 +404,12 @@ public sealed class Settings
{
var lines = GameData.Instance.Vehicles.CollectMissingAssetLines();
if (lines.Count == 0) return;
string dir = Path.Combine(Directory.GetCurrentDirectory(), "starter");
Directory.CreateDirectory(dir);
File.WriteAllLines(Path.Combine(dir, "bledy.txt"), lines);
Infrastructure.Diagnostics.Log(lines);
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log("Missing vehicle log", ex);
}
catch { }
}
private void ReadFromConfig()
@@ -492,7 +503,10 @@ public sealed class Settings
EnableTraction = c.GetBool("enabletraction", true);
LiveTraction = c.GetBool("livetraction", true);
PhysicsLog = c.GetBool("physicslog", false);
DebugLog = c.GetInt("debuglog", 3) != 0;
int debugLog = c.GetInt("debuglog", 3);
DebugLog = (debugLog & 1) != 0;
DebugLogVisible = (debugLog & 2) != 0;
_debugLogExtraBits = debugLog & ~3;
MultipleLogs = c.GetBool("multiplelogs", false);
DisplaySimulation = !c.GetBool("gfx.skiprendering", false);
CrashDamage = c.GetBool("crashdamage", true);
@@ -506,6 +520,9 @@ public sealed class Settings
VehiclesVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.vehicle", 1.0) * 100), 1, 100);
PositionalVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.positional", 1.0) * 100), 1, 100);
AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 1.0) * 100), 1, 100);
VolumePaused = Clamp((int)Math.Round(c.GetDouble("sound.volume.paused", 1.0) * 100), 0, 100);
SkipPipeline = c.GetBool("gfx.skippipeline", false);
PyScreenPriority = c.GetString("pyscreenrendererpriority", "normal");
CompressTextures = c.GetBool("compresstex", true);
ScaleSpeculars = c.GetBool("scalespeculars", true);
@@ -613,7 +630,8 @@ public sealed class Settings
c.SetBool("enabletraction", EnableTraction);
c.SetBool("livetraction", LiveTraction);
c.SetBool("physicslog", PhysicsLog);
c.SetInt("debuglog", DebugLog ? 3 : 0);
c.SetInt("debuglog",
(DebugLog ? 1 : 0) | (DebugLogVisible ? 2 : 0) | _debugLogExtraBits);
c.SetBool("multiplelogs", MultipleLogs);
c.SetBool("gfx.skiprendering", !DisplaySimulation);
c.SetBool("crashdamage", CrashDamage);
@@ -627,6 +645,9 @@ public sealed class Settings
c.SetDouble("sound.volume.vehicle", VehiclesVolume / 100.0);
c.SetDouble("sound.volume.positional", PositionalVolume / 100.0);
c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0);
c.SetDouble("sound.volume.paused", VolumePaused / 100.0);
c.SetBool("gfx.skippipeline", SkipPipeline);
c.Set("pyscreenrendererpriority", PyScreenPriority);
c.SetBool("compresstex", CompressTextures);
c.SetBool("scalespeculars", ScaleSpeculars);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -107,7 +107,7 @@ public class Trainset
nodeDynamic.Name = tokens[++i];
i++;
nodeDynamic.DataFolder = tokens[++i];
nodeDynamic.SkinFile = tokens[++i];
nodeDynamic.SkinFile = Dynamic.StripSkinExtension(tokens[++i]);
nodeDynamic.MmdFile = tokens[++i];
nodeDynamic.Offset = float.Parse(tokens[++i], CultureInfo.InvariantCulture);
@@ -158,30 +158,7 @@ public class Trainset
string entry = sb.ToString();
foreach (Dynamic vehicle in Vehicles)
{
string driverType = "";
switch (vehicle.DriverType)
{
case eDriverType.Headdriver:
driverType = "headdriver";
break;
case eDriverType.Reardriver:
driverType = "reardriver";
break;
case eDriverType.Passenger:
driverType = "passenger";
break;
default:
driverType = "nobody";
break;
}
entry +=
$"node {vehicle.RangeMax} {vehicle.RangeMin} {vehicle.Name} dynamic " +
$"{vehicle.DataFolder} {vehicle.SkinFile} {vehicle.MmdFile} " +
$"{vehicle.Offset.ToString(CultureInfo.InvariantCulture)} " +
$"{driverType} {vehicle.Coupling}{vehicle.WriteTrailing()} enddynamic\n";
}
entry += vehicle.ToTrainsetNode();
return entry;
}
@@ -207,7 +184,11 @@ public class Dynamic
set => Coupling.Flags = value;
}
public string? MaxLoad;
public const string PantState = "pantstate";
public const int PantStateMax = 3;
public int MaxLoad = -1;
public bool HasVelocity;
@@ -217,25 +198,24 @@ public class Dynamic
public string? LoadType;
public List<string> Destinations = new();
public string? MiniName;
public bool Flipped;
public static bool IsPantStateType(string? type) =>
string.Equals(type, PantState, StringComparison.OrdinalIgnoreCase);
public bool IsPantState => IsPantStateType(LoadType);
internal void ReadTrailing(List<string> t)
{
int p = 0;
if (p < t.Count && (t[p][0] == 'L' || t[p][0] == 'l')
&& t[p].Length > 1 && char.IsDigit(t[p][1]))
MaxLoad = t[p++];
if (p < t.Count && float.TryParse(t[p], NumberStyles.Float,
CultureInfo.InvariantCulture, out float v))
if (p < t.Count && t[p].Length > 0 && t[p][0] == 'L')
{
Velocity = v;
HasVelocity = true;
if (int.TryParse(t[p].AsSpan(1), NumberStyles.Integer,
CultureInfo.InvariantCulture, out int ml))
MaxLoad = ml;
p++;
}
@@ -244,29 +224,30 @@ public class Dynamic
{
LoadCount = lc;
p++;
if (lc > 0 && p < t.Count)
if (p < t.Count && (lc > 0 || IsPantStateType(t[p])))
LoadType = t[p++];
}
while (p < t.Count)
Destinations.Add(t[p++]);
}
internal static string StripSkinExtension(string token)
{
int dot = token.LastIndexOf('.');
return dot > 0 ? token[..dot] : token;
}
internal string WriteTrailing()
{
var sb = new StringBuilder();
if (MaxLoad != null)
sb.Append(' ').Append(MaxLoad);
if (HasVelocity)
sb.Append(' ').Append(Velocity.ToString(CultureInfo.InvariantCulture));
if (LoadCount > 0)
{
sb.Append(' ').Append(LoadCount.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrEmpty(LoadType))
sb.Append(' ').Append(LoadType);
}
foreach (string d in Destinations)
sb.Append(' ').Append(d);
if (MaxLoad >= 0)
sb.Append(" L").Append(MaxLoad.ToString(CultureInfo.InvariantCulture));
bool loaded = !string.IsNullOrEmpty(LoadType);
sb.Append(' ').Append((loaded ? LoadCount : 0).ToString(CultureInfo.InvariantCulture));
if (loaded && (LoadCount > 0 || IsPantState))
sb.Append(' ').Append(LoadType);
return sb.ToString();
}
@@ -287,11 +268,27 @@ public class Dynamic
Velocity = Velocity,
LoadCount = LoadCount,
LoadType = LoadType,
Destinations = new List<string>(Destinations),
MiniName = MiniName,
Flipped = Flipped
};
public string ToTrainsetNode()
{
string driver = DriverType switch
{
eDriverType.Headdriver => "headdriver",
eDriverType.Reardriver => "reardriver",
eDriverType.Passenger => "passenger",
_ => "nobody"
};
return
$"node {RangeMax} {RangeMin} {Name} dynamic " +
$"{DataFolder} {SkinFile} {MmdFile} " +
$"{Offset.ToString(CultureInfo.InvariantCulture)} " +
$"{driver} {Coupling}{WriteTrailing()} enddynamic\n";
}
public string ToLooseNode()
{
string driver = DriverType switch
@@ -374,7 +371,7 @@ public sealed class Coupling
public void SetBrake(BrakeSetting? brake)
{
Parameters.RemoveAll(p => p.StartsWith("B", StringComparison.Ordinal));
if (brake != null)
if (brake is { IsEmpty: false })
Parameters.Insert(0, brake.ToParameter());
}
@@ -417,34 +414,39 @@ public sealed class Coupling
public sealed class BrakeSetting
{
public static readonly string[] Modes = { "R+Mg", "G", "P", "R", "Q", "O", "A" };
public static readonly string[] Modes = { "G", "P", "R", "M", "Q" };
public string Mode = "P";
public static readonly string[] Loads = { "T", "H", "F" };
public static readonly string[] Switches = { "0", "1" };
public string? Mode;
public string? Load;
public string? Switch;
public bool IsEmpty => Mode is null;
public static BrakeSetting? FromParameter(string? param)
{
if (string.IsNullOrEmpty(param) || param![0] != 'B' || param.Length < 2)
if (string.IsNullOrEmpty(param) || char.ToUpperInvariant(param![0]) != 'B')
return null;
string body = param.Substring(1);
string? mode = Modes.FirstOrDefault(m => body.StartsWith(m, StringComparison.Ordinal));
if (mode is null)
return null;
string body = param.Substring(1).ToUpperInvariant();
var b = new BrakeSetting
{
Mode = First(body, "M", "G", "P", "R", "Q"),
Load = First(body, Loads),
Switch = First(body, Switches)
};
return b.Mode is null && b.Load is null && b.Switch is null ? null : b;
var b = new BrakeSetting { Mode = mode };
int p = mode.Length;
if (p < body.Length && "THFA".IndexOf(body[p]) >= 0)
b.Load = body[p++].ToString();
if (p < body.Length && "01A".IndexOf(body[p]) >= 0)
b.Switch = body[p].ToString();
return b;
static string? First(string body, params string[] codes) =>
Array.Find(codes, c => body.Contains(c, StringComparison.Ordinal));
}
public string ToParameter() => "B" + Mode + (Load ?? "") + (Switch ?? "");
public string ToParameter() => "B" + Mode + Switch + Load;
}
public sealed class WheelSettings

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -173,6 +173,26 @@ public class VehicleDatabase
}
}
public int ImportLegacy(string dynamicRoot = "dynamic")
{
int added = 0;
foreach (string file in LegacyTextureDatabase.EnumerateFiles(dynamicRoot))
{
var entry = LegacyTextureDatabase.Parse(file, dynamicRoot);
if (entry is null) continue;
entry.Textures.RemoveAll(t =>
TextureBySkin.ContainsKey(
Path.GetFileNameWithoutExtension(t.Skinfile).ToLowerInvariant()));
if (entry.Textures.Count == 0) continue;
added += entry.Textures.Count;
Ingest(entry);
}
return added;
}
private void Ingest(VehicleEntry entry)
{
foreach (var group in entry.Groups)
@@ -265,7 +285,7 @@ public class VehicleDatabase
public string? ResolveMiniName(VehicleTexture texture)
{
if (!string.IsNullOrEmpty(texture.TextureMini) && MiniPath(texture.TextureMini) != null)
if (HasMini(texture.TextureMini))
return texture.TextureMini;
if (texture.Group != null && GroupsById.TryGetValue(texture.Group, out var grp)
@@ -330,10 +350,26 @@ public class VehicleDatabase
public static string? MiniPath(string? miniName, string miniDir = "textures/mini/")
{
if (string.IsNullOrEmpty(miniName)) return null;
var index = _miniIndex ??= BuildMiniIndex(miniDir);
return index.TryGetValue(miniName!.ToLowerInvariant(), out var path) ? path : null;
if (!string.IsNullOrEmpty(miniName) &&
index.TryGetValue(miniName!.ToLowerInvariant(), out var path))
return path;
return FallbackMiniPath(miniDir);
}
public static bool HasMini(string? miniName, string miniDir = "textures/mini/")
{
if (string.IsNullOrEmpty(miniName)) return false;
var index = _miniIndex ??= BuildMiniIndex(miniDir);
return index.ContainsKey(miniName!.ToLowerInvariant());
}
public static string? FallbackMiniPath(string miniDir = "textures/mini/")
{
var index = _miniIndex ??= BuildMiniIndex(miniDir);
return index.TryGetValue("other", out var other) ? other : null;
}
private static Dictionary<string, string> BuildMiniIndex(string miniDir)

View File

@@ -19,7 +19,6 @@ public static class TrainsetDisplay
if (IsEmpty(trainset))
return null;
var db = GameData.Instance.Vehicles;
var sb = new StringBuilder();
string? prevKey = null;
int run = 0;
@@ -36,22 +35,16 @@ public static class TrainsetDisplay
for (int i = 0; i < trainset.Vehicles.Count; i++)
{
var v = trainset.Vehicles[i];
var tex = db.TextureForSkin(v.SkinFile);
string cat = tex?.ResolvedCategory ?? "";
bool powered = cat is "e" or "s" or "p" or "z" or "a";
if (i == 0 && powered)
if (i == 0 && IsPowered(v))
{
string head = !string.IsNullOrWhiteSpace(v.Name) ? v.Name! :
(!string.IsNullOrEmpty(tex?.ResolvedClass) ? tex!.ResolvedClass : BaseSkin(v));
sb.Append(head);
sb.Append(VehicleLabel(v, head: true));
prevKey = null;
run = 0;
continue;
}
string key = !string.IsNullOrEmpty(tex?.ResolvedClass) ? tex!.ResolvedClass
: (!string.IsNullOrWhiteSpace(v.Name) ? v.Name! : BaseSkin(v));
string key = VehicleLabel(v);
if (string.Equals(key, prevKey, StringComparison.OrdinalIgnoreCase))
{
@@ -69,6 +62,18 @@ public static class TrainsetDisplay
!string.IsNullOrWhiteSpace(v.Name) ? v.Name! : BaseSkin(v)));
}
public static string VehicleLabel(Dynamic v, bool head = false)
{
var tex = GameData.Instance.Vehicles.TextureForSkin(v.SkinFile);
string? cls = string.IsNullOrEmpty(tex?.ResolvedClass) ? null : tex!.ResolvedClass;
string? name = string.IsNullOrWhiteSpace(v.Name) ? null : v.Name;
return (head ? name ?? cls : cls ?? name) ?? BaseSkin(v);
}
public static bool IsPowered(Dynamic v) =>
GameData.Instance.Vehicles.TextureForSkin(v.SkinFile)?.ResolvedCategory
is "e" or "s" or "p" or "z" or "a";
private static string BaseSkin(Dynamic v) =>
string.IsNullOrWhiteSpace(v.SkinFile) ? "?" :
System.IO.Path.GetFileNameWithoutExtension(v.SkinFile);
@@ -87,6 +92,33 @@ public static class TrainsetDisplay
return staffed?.Name ?? trainset.Vehicles[0].Name;
}
public static List<(string Label, string Value)> StatsFields(
Trainset? trainset, Func<Dynamic, Physics?> physicsFor,
Func<Dynamic, string?>? categoryOf = null,
Func<string?, int>? loadWeightKg = null)
{
var fields = new List<(string, string)>();
if (trainset is null)
return fields;
var (lengthM, massKg, loadKg) = RecalcParams(trainset, physicsFor, categoryOf, loadWeightKg, allVehicles: false);
if (massKg < 200_000)
(lengthM, massKg, loadKg) = RecalcParams(trainset, physicsFor, categoryOf, loadWeightKg, allVehicles: true);
var inv = CultureInfo.InvariantCulture;
string track = trainset.Track ?? "";
if (trainset.Velocity > 0)
track = $"{track} ({trainset.Velocity.ToString("0.#", inv)} km/h)";
fields.Add(($"{App.Loc["Mass"]} [t]:", (massKg / 1000.0).ToString("0.#", inv)));
fields.Add(($"{App.Loc["MassBrutto"]} [t]:", ((massKg + loadKg) / 1000.0).ToString("0.#", inv)));
fields.Add(($"{App.Loc["Length"]} [m]:", lengthM.ToString("0.#", inv)));
fields.Add(($"{App.Loc["VehicleCount"]}:", trainset.Vehicles.Count.ToString(inv)));
fields.Add(($"{App.Loc["Track"]}:", track));
return fields;
}
public static string FormatStats(Trainset? trainset, Func<Dynamic, Physics?> physicsFor,
string lengthLabel, string massLabel, string vehiclesLabel, string trackLabel,
Func<Dynamic, string?>? categoryOf = null,
@@ -154,7 +186,7 @@ public static class TrainsetDisplay
private static bool LoadAccepted(Physics? p, string loadType)
{
if (p == null || string.IsNullOrWhiteSpace(p.LoadAccepted))
return true;
return false;
return p.LoadAccepted.Contains(loadType, StringComparison.OrdinalIgnoreCase);
}
@@ -180,20 +212,12 @@ public static class TrainsetDisplay
if (mu)
{
foreach (var v in trainset.Vehicles)
{
if (!string.IsNullOrEmpty(v.Name))
used.Remove(v.Name!);
v.Name = EnsureUnique(PreferredBaseName(v), used);
}
Rename(v, used);
return target?.Name;
}
if (target != null)
{
if (!string.IsNullOrEmpty(target.Name))
used.Remove(target.Name!);
target.Name = EnsureUnique(PreferredBaseName(target), used);
}
Rename(target, used);
return target?.Name;
}
@@ -251,4 +275,21 @@ public static class TrainsetDisplay
private static string SanitizeName(string name) =>
string.IsNullOrWhiteSpace(name) ? "vehicle" : name.Replace(' ', '_');
public static (string Base, string Suffix) SplitWagonNumber(string? name)
{
string n = name ?? "";
int hash = n.LastIndexOf('#');
if (hash >= 0 && hash + 1 < n.Length && n[(hash + 1)..].All(char.IsDigit))
return (n[..hash], n[hash..]);
return (n, "");
}
private static void Rename(Dynamic v, HashSet<string> used)
{
var (_, number) = SplitWagonNumber(v.Name);
if (!string.IsNullOrEmpty(v.Name))
used.Remove(v.Name!);
v.Name = EnsureUnique(PreferredBaseName(v), used) + number;
}
}

198
StarterNG/Domain/UData.cs Normal file
View File

@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StarterNG.Classes;
namespace StarterNG.Domain;
public static class UData
{
public readonly record struct TrainParams(double Length, double Mass, double LoadMass, int CountVehicles);
public static int GetMaxCoupler(Dynamic v, Func<Dynamic, Physics?> physicsFor, bool leftCoupler = true)
{
var fiz = physicsFor(v);
if (fiz == null) return 3;
return leftCoupler
? (v.Offset >= 0 ? fiz.AllowedFlagA : fiz.AllowedFlagB)
: (v.Offset >= 0 ? fiz.AllowedFlagB : fiz.AllowedFlagA);
}
public static string GetControlType(Dynamic v, Func<Dynamic, Physics?> physicsFor, bool leftCoupler = true)
{
var fiz = physicsFor(v);
if (fiz == null) return "";
return leftCoupler
? (v.Offset >= 0 ? fiz.ControlTypeA : fiz.ControlTypeB)
: (v.Offset >= 0 ? fiz.ControlTypeB : fiz.ControlTypeA);
}
public static List<int> CheckFlag(int flag)
{
var result = new List<int>();
for (int f = 128; f >= 1; f >>= 1)
if (flag >= f) { result.Add(f); flag -= f; }
return result;
}
public static int CommonCoupler(int c1, int c2)
{
var f1 = CheckFlag(c1);
var f2 = CheckFlag(c2);
return f1.Where(f2.Contains).Sum();
}
public static void AutoConnect(IList<Dynamic> vehicles, int leftVehicle,
Func<Dynamic, Physics?> physicsFor)
{
if (leftVehicle < 0 || vehicles.Count - 1 <= leftVehicle) return;
int leftMax = GetMaxCoupler(vehicles[leftVehicle], physicsFor, leftCoupler: false);
string controlType1 = GetControlType(vehicles[leftVehicle], physicsFor, leftCoupler: false);
int rightMax = GetMaxCoupler(vehicles[leftVehicle + 1], physicsFor);
string controlType2 = GetControlType(vehicles[leftVehicle + 1], physicsFor);
int coupler = CommonCoupler(leftMax, rightMax);
const int F4 = 4;
if (CheckFlag(coupler).Contains(F4) &&
!string.Equals(controlType1, controlType2, StringComparison.OrdinalIgnoreCase))
coupler -= F4;
vehicles[leftVehicle].Coupling.Flags = coupler;
}
public static List<int> GetMultiple(IList<Dynamic> vehicles, int index,
Func<Dynamic, string?> setIdOf)
{
var result = new List<int> { index };
bool Same(int a, int b)
{
string? x = setIdOf(vehicles[a]);
return x != null && x == setIdOf(vehicles[b]);
}
int i = 0;
while (index - i > 0 && Same(index - i, index - i - 1))
{
result.Add(index - i - 1);
i++;
}
i = 0;
while (vehicles.Count - 1 > index + i && Same(index + i, index + i + 1))
{
result.Add(index + i + 1);
i++;
}
return result;
}
public static bool StaffedTrain(Trainset train) =>
train.Vehicles.Count > 0 &&
(IsDriver(train.Vehicles[0].DriverType) || IsDriver(train.Vehicles[^1].DriverType));
private static bool IsDriver(eDriverType d) =>
d is eDriverType.Headdriver or eDriverType.Reardriver;
public static bool IncludeVehicleToMass(Dynamic v, bool allVehicles,
Func<Dynamic, Physics?> physicsFor, Func<Dynamic, string?> categoryOf)
{
if (physicsFor(v) == null) return false;
if (allVehicles) return true;
bool notDriver = v.DriverType is eDriverType.Passenger or eDriverType.Nobody;
string? cat = categoryOf(v);
return notDriver || cat is "z" or "a";
}
public static TrainParams RecalcTrainParams(Trainset? train, bool allVehicles,
Func<Dynamic, Physics?> physicsFor, Func<Dynamic, string?> categoryOf,
Func<string?, int> loadWeight)
{
double length = 0, mass = 0, loadMass = 0;
if (train == null) return new TrainParams(0, 0, 0, 0);
for (int i = train.Vehicles.Count - 1; i >= 0; i--)
{
var v = train.Vehicles[i];
var fiz = physicsFor(v);
if (IncludeVehicleToMass(v, allVehicles, physicsFor, categoryOf) && fiz != null)
{
mass += fiz.Mass;
if (!string.IsNullOrEmpty(v.LoadType) &&
fiz.LoadAccepted.Contains(v.LoadType!, StringComparison.OrdinalIgnoreCase))
loadMass += v.LoadCount * loadWeight(v.LoadType);
else if (!string.IsNullOrEmpty(v.LoadType))
Infrastructure.Diagnostics.Log($"{v.Name} - nieobsługiwany ładunek pojazdu");
}
if (fiz != null)
length += fiz.Length;
}
return new TrainParams(length, mass, loadMass, train.Vehicles.Count);
}
public static void RandomLoad(IEnumerable<Dynamic> vehicles,
Func<Dynamic, Physics?> physicsFor, Random rng)
{
foreach (var v in vehicles)
{
var fiz = physicsFor(v);
if (fiz == null) continue;
var loads = fiz.LoadAccepted
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.ToList();
if (loads.Count > 0)
v.LoadType = loads[rng.Next(loads.Count)];
}
}
public static void ReverseMultiple(IList<Dynamic> vehicles, int position,
Func<Dynamic, string?> setIdOf, Func<Dynamic, Physics?> physicsFor)
{
var indexes = GetMultiple(vehicles, position, setIdOf);
foreach (int i in indexes)
vehicles[i].Offset = vehicles[i].Offset >= 0 ? -1f : 0f;
indexes.Sort();
int first = indexes[0], last = indexes[^1];
for (int i = 0; i < indexes.Count / 2; i++)
(vehicles[first + i], vehicles[last - i]) = (vehicles[last - i], vehicles[first + i]);
foreach (int i in indexes)
AutoConnect(vehicles, i, physicsFor);
}
public static bool CheckMoveVehicle(IList<Dynamic> vehicles, int fromPos, int toPos,
Func<Dynamic, string?> setIdOf)
{
bool Same(int a, int b)
{
if (a < 0 || b < 0 || a >= vehicles.Count || b >= vehicles.Count) return false;
string? x = setIdOf(vehicles[a]);
return x != null && x == setIdOf(vehicles[b]);
}
if (fromPos < toPos)
{
if (toPos > 0 && toPos < vehicles.Count - 1 && Same(toPos, toPos + 1))
return false;
}
else if (toPos > 0 && Same(toPos, toPos - 1))
{
return false;
}
return true;
}
}

153
StarterNG/Domain/UDepot.cs Normal file
View File

@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using StarterNG.Classes;
using StarterNG.Infrastructure;
namespace StarterNG.Domain;
public static class UDepot
{
public sealed class DepotTrain
{
public string TrainName = "";
public List<Dynamic> Vehicles = new();
}
public static string? FindDepotFile(string? path = null)
{
if (!string.IsNullOrEmpty(path))
return File.Exists(path) ? path : null;
string root = Directory.GetCurrentDirectory();
foreach (string rel in new[] { Path.Combine("starter", "magazyn.ini"), "starter.ini", "RAINSTED.INI" })
{
string full = Path.Combine(root, rel);
if (File.Exists(full)) return full;
}
return null;
}
public static List<DepotTrain> ReadDepot(string? path = null)
{
var result = new List<DepotTrain>();
string? file = FindDepotFile(path);
if (file == null) return result;
string text;
try
{
text = File.ReadAllText(file, Encoding.GetEncoding(1250));
}
catch (Exception ex)
{
Diagnostics.Log($"Błąd parsowania magazynu {file}", ex);
return result;
}
DepotTrain? current = null;
int line = 0;
foreach (string raw in text.Replace("\r\n", "\n").Split('\n'))
{
line++;
string s = raw.Trim();
if (s.Length == 0) continue;
var header = Regex.Match(s, @"^\[\s*TRAINSET\d+\s*(?:=\s*([^\]]*))?\]", RegexOptions.IgnoreCase);
if (header.Success)
{
current = new DepotTrain { TrainName = header.Groups[1].Value.Trim() };
result.Add(current);
continue;
}
if (current == null) continue;
var entry = Regex.Match(s, @"^\d+\s*=\s*(.*)$");
string body = entry.Success ? entry.Groups[1].Value.Trim() : s;
if (!body.StartsWith("node", StringComparison.OrdinalIgnoreCase)) continue;
try
{
var v = ParseNode(body);
if (v != null) current.Vehicles.Add(v);
}
catch (Exception ex)
{
Diagnostics.Log($"Błąd parsowania magazynu. Linia: {line}", ex);
}
}
result.RemoveAll(t => t.Vehicles.Count == 0);
return result;
}
private static Dynamic? ParseNode(string body)
{
var t = body.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
if (t.Length < 10) return null;
var inv = CultureInfo.InvariantCulture;
int p = 1;
var d = new Dynamic
{
RangeMax = float.Parse(t[p++], inv),
RangeMin = float.Parse(t[p++], inv),
Name = t[p++]
};
p++;
d.DataFolder = t[p++];
d.SkinFile = Dynamic.StripSkinExtension(t[p++]);
d.MmdFile = t[p++];
d.Offset = float.Parse(t[p++], inv);
d.DriverType = t[p++].ToLowerInvariant() switch
{
"headdriver" => eDriverType.Headdriver,
"reardriver" => eDriverType.Reardriver,
"passenger" => eDriverType.Passenger,
_ => eDriverType.Nobody
};
d.Coupling = Coupling.Parse(t[p++]);
var trailing = new List<string>();
while (p < t.Length && !string.Equals(t[p], "enddynamic", StringComparison.OrdinalIgnoreCase))
trailing.Add(t[p++]);
d.ReadTrailing(trailing);
return d;
}
public static bool SaveDepot(IReadOnlyList<DepotTrain> depot, string? path = null)
{
path ??= Path.Combine(Directory.GetCurrentDirectory(), "starter", "magazyn.ini");
try
{
var sb = new StringBuilder();
for (int i = 0; i < depot.Count; i++)
{
if (depot[i].Vehicles.Count == 0) continue;
sb.Append('[').Append("TRAINSET").Append(i).Append('=')
.Append(depot[i].TrainName).Append("]\n");
for (int y = 0; y < depot[i].Vehicles.Count; y++)
sb.Append(y.ToString("00", CultureInfo.InvariantCulture)).Append('=')
.Append(depot[i].Vehicles[y].ToTrainsetNode());
}
string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(path, sb.ToString(), Encoding.GetEncoding(1250));
return true;
}
catch (Exception ex)
{
Diagnostics.Log("Błąd zapisu magazynu", ex);
return false;
}
}
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using StarterNG.Views;
namespace StarterNG.Infrastructure;
public static class Diagnostics
{
private static readonly object Gate = new();
private static readonly List<string> Lines = new();
public static string LogPath =>
Path.Combine(Directory.GetCurrentDirectory(), "starter", "bledy.txt");
public static string Text
{
get { lock (Gate) return string.Join(Environment.NewLine, Lines); }
}
public static void Clear()
{
lock (Gate) Lines.Clear();
}
public static void Log(string message)
{
lock (Gate)
Lines.Add($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)} {message}");
}
public static void Log(IEnumerable<string> messages)
{
foreach (string message in messages)
Log(message);
}
public static void Log(string context, Exception ex) =>
Log($"{context}: {ex.GetType().Name}: {ex.Message}");
public static void Flush()
{
string[] snapshot;
lock (Gate)
{
if (Lines.Count == 0) return;
snapshot = Lines.ToArray();
}
try
{
string dir = Path.GetDirectoryName(LogPath)!;
Directory.CreateDirectory(dir);
File.WriteAllLines(LogPath, snapshot);
}
catch
{
}
}
private static Window? Owner =>
(Avalonia.Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
?.MainWindow;
public static Task ReportAsync(string message, string? title = null)
{
Log(message.Replace(Environment.NewLine, " | "));
var owner = Owner;
if (owner is null)
return Task.CompletedTask;
return MessageBox.Show(owner, message, title ?? App.Loc["FaultTitle"], MessageBoxButtons.Ok);
}
public static Task ReportAsync(string context, Exception ex, string? title = null) =>
ReportAsync($"{context}{Environment.NewLine}{Environment.NewLine}{App.Loc["FaultDetail"]} {ex.Message}", title);
public static void ReportOnUiThread(string message, string? title = null)
{
if (Dispatcher.UIThread.CheckAccess())
_ = ReportAsync(message, title);
else
Dispatcher.UIThread.Post(() => _ = ReportAsync(message, title));
}
public static List<string> CheckInstallation()
{
var faults = new List<string>();
string root = Directory.GetCurrentDirectory();
foreach (string folder in new[] { "dynamic", "sounds", "models", "scenery", "textures" })
if (!Directory.Exists(Path.Combine(root, folder)))
faults.Add(string.Format(App.Loc["FaultNoDir"], "/" + folder));
if (!File.Exists(Path.Combine(root, "data", "load_weights.txt")))
faults.Add(App.Loc["FaultNoWeights"]);
if (Classes.GameData.Instance.Sceneries.Count == 0)
faults.Add(App.Loc["FaultNoScenery"]);
if (Classes.GameData.Instance.Vehicles.Textures.Count == 0)
faults.Add(App.Loc["FaultNoVehicles"]);
if (Classes.Physics.IndexedCount == 0)
faults.Add(App.Loc["FaultNoPhysics"]);
if (Classes.Settings.Instance.ResolveExecutable(out var problem) is var _ &&
problem == Classes.ExeProblem.NotFound)
faults.Add(App.Loc["FaultNoExe"]);
return faults;
}
}

View File

@@ -0,0 +1,67 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
namespace StarterNG.Infrastructure;
public sealed class DropGap
{
private readonly Border _gap;
private int _index = -1;
public DropGap(int milliseconds = 130)
{
_gap = new Border
{
Width = 0,
IsHitTestVisible = false,
Transitions = new Transitions
{
new DoubleTransition
{
Property = Layoutable.WidthProperty,
Duration = TimeSpan.FromMilliseconds(milliseconds),
Easing = new CubicEaseOut()
}
}
};
}
public bool IsGap(Control control) => ReferenceEquals(control, _gap);
public void Show(Panel panel, int childIndex, double width)
{
int target = Math.Clamp(childIndex, 0, ChildCountWithoutGap(panel));
if (_index != target)
{
panel.Children.Remove(_gap);
panel.Children.Insert(Math.Clamp(target, 0, panel.Children.Count), _gap);
_index = target;
}
_gap.Width = Math.Max(width, 8);
}
public void Hide(Panel panel)
{
if (_index < 0) return;
_gap.Width = 0;
panel.Children.Remove(_gap);
_index = -1;
}
public void Forget() => _index = -1;
private int ChildCountWithoutGap(Panel panel)
{
int count = panel.Children.Count;
foreach (var child in panel.Children)
if (ReferenceEquals(child, _gap))
return count - 1;
return count;
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
namespace StarterNG.Infrastructure;
public sealed class DropIndexTracker
{
private const double Hysteresis = 14;
private static readonly TimeSpan Quiet = TimeSpan.FromMilliseconds(60);
private readonly List<double> _midpoints = new();
private DateTime _changedAt = DateTime.MinValue;
public int Index { get; private set; } = -1;
public void Capture(IEnumerable<double> midpoints)
{
_midpoints.Clear();
_midpoints.AddRange(midpoints);
Index = -1;
_changedAt = DateTime.MinValue;
}
public void Reset()
{
_midpoints.Clear();
Index = -1;
}
public bool Update(double x)
{
int candidate = Index;
if (candidate < 0)
{
candidate = 0;
while (candidate < _midpoints.Count && x > _midpoints[candidate])
candidate++;
}
else
{
while (candidate < _midpoints.Count && x > _midpoints[candidate] + Hysteresis)
candidate++;
while (candidate > 0 && x < _midpoints[candidate - 1] - Hysteresis)
candidate--;
}
if (candidate == Index)
return false;
var now = DateTime.UtcNow;
if (Index >= 0 && now - _changedAt < Quiet)
return false;
_changedAt = now;
Index = candidate;
return true;
}
}

View File

@@ -0,0 +1,63 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
namespace StarterNG.Infrastructure;
public sealed class EdgeScroller
{
private const double Zone = 56;
private const double MaxStep = 18;
private readonly DispatcherTimer _timer;
private ScrollViewer? _target;
private double _step;
public EdgeScroller()
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) };
_timer.Tick += (_, _) => Step();
}
public void Update(ScrollViewer scroll, double pointerX)
{
double width = scroll.Viewport.Width;
_step = pointerX < Zone
? -MaxStep * Math.Clamp((Zone - pointerX) / Zone, 0, 1)
: pointerX > width - Zone
? MaxStep * Math.Clamp((pointerX - (width - Zone)) / Zone, 0, 1)
: 0;
_target = scroll;
if (_step == 0)
_timer.Stop();
else if (!_timer.IsEnabled)
_timer.Start();
}
public void Stop()
{
_timer.Stop();
_target = null;
_step = 0;
}
private void Step()
{
if (_target is null || _step == 0)
{
_timer.Stop();
return;
}
double max = Math.Max(0, _target.Extent.Width - _target.Viewport.Width);
double x = Math.Clamp(_target.Offset.X + _step, 0, max);
if (Math.Abs(x - _target.Offset.X) < 0.01)
return;
_target.Offset = new Vector(x, _target.Offset.Y);
}
}

View File

@@ -29,9 +29,9 @@ public static class LoadingScreen
WriteBmp(src, dest);
}
catch
catch (Exception ex)
{
Diagnostics.Log("Loading screen logo", ex);
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace StarterNG.Infrastructure;
public static class StatsBar
{
public static void Fill(Panel host, IReadOnlyList<(string Label, string Value)> fields)
{
host.Children.Clear();
foreach (var (label, value) in fields)
{
var pair = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 5,
Margin = new Avalonia.Thickness(0, 0, 22, 0),
VerticalAlignment = VerticalAlignment.Center
};
pair.Children.Add(new TextBlock
{
Text = label,
FontSize = 12,
Opacity = 0.65,
VerticalAlignment = VerticalAlignment.Center
});
pair.Children.Add(new TextBlock
{
Text = value,
FontSize = 12,
FontWeight = FontWeight.SemiBold,
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis
});
host.Children.Add(pair);
}
}
}

View File

@@ -10,36 +10,21 @@
xmlns:starterNg="clr-namespace:StarterNG"
Title="Starter MaSzyna"
Icon="/Assets/starter.ico"
Width="1200" Height="768" MinWidth="1200" MinHeight="666"
Width="1010" Height="920" MinWidth="1010" MinHeight="920"
WindowStartupLocation="CenterScreen">
<!-- The client area is extended over the title bar in code-behind (the
ExtendClientArea* hints), so the logo + nav sit on the top bar while the
standard minimise / maximise / close buttons stay in the top-right corner.
Extending the client area also makes Avalonia 12 add an extra full-screen caption
button; it is hidden in App.axaml via its WindowDecorationProperties.ElementRole. -->
<DockPanel LastChildFill="True">
<!-- ── Top bar: logo, navigation and the right cluster in one row.
On Windows the OS caption buttons are drawn over the top of the
extended client area, so TopBar is padded down to clear them
(see the constructor); the free strip drags the window. -->
<Border x:Name="TopBar" DockPanel.Dock="Top"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="0,0,0,1"
PointerPressed="TitleBar_OnPointerPressed">
<Grid RowDefinitions="48">
<Grid RowDefinitions="50">
<!-- Logo + navigation (left) and the right cluster (right) -->
<Grid ColumnDefinitions="Auto,Auto,*,Auto" Background="Transparent">
<Grid ColumnDefinitions="Auto,*,Auto" Background="Transparent">
<!-- Logo -->
<Image Grid.Column="0" Source="/Assets/Logo/maszyna_white.png"
Height="34" Margin="14,0,18,0" VerticalAlignment="Center" />
<!-- Page tabs -->
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Stretch">
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Stretch">
<RadioButton x:Name="NavScenarios" GroupName="nav" Theme="{StaticResource NavTab}"
Tag="scenarios" IsChecked="True"
IsCheckedChanged="Nav_OnCheckedChanged"
@@ -64,22 +49,18 @@
HorizontalContentAlignment="Center" />
</StackPanel>
<!-- Right cluster: version, language, social links. -->
<StackPanel Grid.Column="3" Orientation="Horizontal"
VerticalAlignment="Center" Margin="0,0,8,0" Spacing="4">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,8,0">
<StackPanel Grid.Column="2" Orientation="Horizontal"
VerticalAlignment="Center" Margin="0,0,3,0" Spacing="0">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,6,0">
<Label Content="{Binding [Version], Source={x:Static starterNg:App.Loc}}"
FontSize="10" Foreground="{StaticResource CarbonFgDim}" Padding="0" />
<Label Content="{x:Static starterNg:AppInfo.Version}"
FontSize="10" Foreground="{StaticResource CarbonFg}" Padding="0" />
</StackPanel>
<ComboBox x:Name="LangCombo" Width="110" VerticalAlignment="Center"
<ComboBox x:Name="LangCombo" Width="88" VerticalAlignment="Center"
SelectionChanged="LangCombo_OnSelectionChanged" />
<Button Classes="Basic" Cursor="Hand" Tag="http://eu07.pl/" Click="linkClick">
<materialIcons:MaterialIcon Kind="Web" />
</Button>
<Button Classes="Basic" Cursor="Hand" Tag="https://discord.gg/eu07" Click="linkClick">
<PathIcon Data="{StaticResource DiscordLogo}" Height="12" />
</Button>
@@ -92,18 +73,22 @@
<Button Classes="Basic" Cursor="Hand" Tag="http://github.com/MaSzyna-EU07/" Click="linkClick">
<materialIcons:MaterialIcon Kind="Github" />
</Button>
<Button Classes="Basic" Cursor="Hand" Tag="http://eu07.pl/" Click="linkClick"
Padding="6,0" Margin="4,0,0,0"
ToolTip.Tip="eu07.pl">
<Image Source="/Assets/Logo/maszyna_white.png" Height="26" />
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
<!-- ── Full-width START button (bottom) ─────────────────────────────── -->
<Button Name="startButton" DockPanel.Dock="Bottom" Classes="Start" Height="44" Margin="6"
<Button Name="startButton" DockPanel.Dock="Bottom" Classes="Start" Height="42" Margin="3"
Click="StartButton_OnClick"
Content="{Binding [Start], Source={x:Static starterNg:App.Loc}}" />
<!-- ── Page host (one view visible at a time, like the original PageControl) -->
<Panel Margin="6,6,6,0">
<Panel Margin="3,3,3,0">
<views:Scenarios x:Name="ScenariosView" />
<views:Depot x:Name="DepotView" IsVisible="False" />
<views:Settings x:Name="SettingsView" IsVisible="False" />

View File

@@ -40,9 +40,24 @@ public partial class MainWindow : Window
PopulateLangCombo();
App.Loc.LanguageChanged += OnLanguageChanged;
startButton.AddHandler(PointerReleasedEvent, StartButton_OnRightClick,
RoutingStrategies.Tunnel);
ScenariosView.RandomizeTexturesRequested += async scenery =>
{
await DepotView.RandomizeSceneryTrainsetsAsync(scenery);
ScenariosView.RefreshConsistView();
};
NavSettings.AddHandler(PointerReleasedEvent, NavSettings_OnRightClick,
RoutingStrategies.Tunnel);
ApplyInitialNav();
AppState.Instance.Changed += UpdateStartButton;
Opened += (_, _) =>
{
_openedUtc = DateTime.UtcNow;
UpdateStartButton();
};
@@ -55,6 +70,25 @@ public partial class MainWindow : Window
BeginMoveDrag(e);
}
private void NavSettings_OnRightClick(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton != MouseButton.Right)
return;
var win = new Views.AdvancedSettingsWindow();
win.ToolRequested += tool =>
{
switch (tool)
{
case "category": DepotView.ToolAddAllCategory(); break;
case "mmd": DepotView.ToolAddUniqueMmd(); break;
case "removeall": DepotView.ToolRemoveAllTrains(); break;
}
};
win.ShowDialog(this);
e.Handled = true;
}
private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e)
{
if (sender is not RadioButton { IsChecked: true } rb)
@@ -63,12 +97,28 @@ public partial class MainWindow : Window
if (ScenariosView is null || DepotView is null || SettingsView is null)
return;
string page = rb.Tag as string ?? "scenarios";
ShowPage(rb.Tag as string);
}
private void ShowPage(string? page)
{
page ??= "scenarios";
ScenariosView.IsVisible = page == "scenarios";
DepotView.IsVisible = page == "depot";
SettingsView.IsVisible = page == "settings";
}
private void ApplyInitialNav()
{
foreach (var rb in new[] { NavScenarios, NavDepot, NavSettings })
if (rb.IsChecked == true)
{
ShowPage(rb.Tag as string);
return;
}
ShowPage("scenarios");
}
private void PopulateLangCombo()
{
LangCombo.Items.Clear();
@@ -163,22 +213,35 @@ public partial class MainWindow : Window
{
target = Path.GetFullPath(pathOrUrl);
if (!File.Exists(target) && !Directory.Exists(target))
{
Diagnostics.ReportOnUiThread(string.Format(App.Loc["FaultFileNotFound"], target));
return;
}
}
Process.Start(new ProcessStartInfo(target) { UseShellExecute = true });
}
catch { }
catch (Exception ex)
{
Diagnostics.Log($"Open {pathOrUrl}", ex);
}
}
private void openLastLogClick(object? sender, RoutedEventArgs e)
private async void openLastLogClick(object? sender, RoutedEventArgs e)
{
string logPath = "log.txt";
if (File.Exists(logPath))
string logPath = Path.GetFullPath("log.txt");
if (!File.Exists(logPath))
{
Process.Start(new ProcessStartInfo(logPath)
{
UseShellExecute = true
});
await Diagnostics.ReportAsync(string.Format(App.Loc["FaultFileNotFound"], logPath));
return;
}
try
{
Process.Start(new ProcessStartInfo(logPath) { UseShellExecute = true });
}
catch (Exception ex)
{
await Diagnostics.ReportAsync(logPath, ex);
}
}
@@ -236,7 +299,28 @@ public partial class MainWindow : Window
App.Loc["ExeLaunchFailed"], MessageBoxButtons.Ok);
}
private async void StartButton_OnClick(object? sender, RoutedEventArgs e)
private async void StartButton_OnClick(object? sender, RoutedEventArgs e) =>
await LaunchAsync(saveSettings: true, freeFly: false);
private void StartButton_OnRightClick(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton != MouseButton.Right)
return;
var menu = new ContextMenu();
var noSave = new MenuItem { Header = App.Loc["StartWithoutSaving"] };
noSave.Click += async (_, _) => await LaunchAsync(saveSettings: false, freeFly: false);
menu.Items.Add(noSave);
var freeFly = new MenuItem { Header = App.Loc["StartFreeFly"] };
freeFly.Click += async (_, _) => await LaunchAsync(saveSettings: true, freeFly: true);
menu.Items.Add(freeFly);
menu.Open(startButton);
}
private async Task LaunchAsync(bool saveSettings, bool freeFly)
{
var scenery = AppState.Instance.CurrentScenery;
if (scenery is null)
@@ -245,7 +329,7 @@ public partial class MainWindow : Window
if (trainset is null)
return;
if (!HasStartableVehicle(trainset) &&
if (!freeFly && !HasStartableVehicle(trainset) &&
!await MessageBox.Show(this, App.Loc["StartNoStaffConfirm"],
App.Loc["StartNoStaffTitle"], MessageBoxButtons.YesNo))
return;
@@ -277,12 +361,14 @@ public partial class MainWindow : Window
{
File.WriteAllText(exportPath, scenery.BuildExportContent(SettingsModel.Instance.IgnoreIrrelevantTrains), Encoding.GetEncoding(1250));
}
catch
catch (Exception ex)
{
await Diagnostics.ReportAsync($"{exportPath}", ex, App.Loc["FaultTitle"]);
return;
}
SettingsModel.Instance.CaptureAndSave();
if (saveSettings)
SettingsModel.Instance.CaptureAndSave();
string exe = Path.GetFullPath(SettingsModel.Instance.ResolveExecutable(out var exeProblem));
if (exeProblem != ExeProblem.None)
@@ -297,7 +383,7 @@ public partial class MainWindow : Window
sim = Process.Start(new ProcessStartInfo
{
FileName = exe,
Arguments = $"-s {exportName} -v {vehicle}",
Arguments = freeFly ? $"-s {exportName}" : $"-s {exportName} -v {vehicle}",
WorkingDirectory = Path.GetDirectoryName(exe) ?? Directory.GetCurrentDirectory(),
UseShellExecute = false
});
@@ -315,8 +401,6 @@ public partial class MainWindow : Window
return;
}
Hide();
GC.Collect();
GC.WaitForPendingFinalizers();
@@ -395,8 +479,9 @@ public partial class MainWindow : Window
private void RestoreFromSimulator()
{
Show();
WindowState = WindowState.Normal;
Activate();
if (!IsVisible)
Show();
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Normal;
}
}

View File

@@ -1,5 +1,7 @@
using Avalonia;
using Avalonia;
using System;
using System.Threading.Tasks;
using StarterNG.Infrastructure;
namespace StarterNG;
@@ -7,8 +9,35 @@ class Program
{
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
if (e.ExceptionObject is Exception ex)
Diagnostics.Log("Unhandled exception", ex);
Diagnostics.Flush();
};
TaskScheduler.UnobservedTaskException += (_, e) =>
{
Diagnostics.Log("Unobserved task exception", e.Exception);
e.SetObserved();
};
try
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
catch (Exception ex)
{
Diagnostics.Log("Fatal", ex);
throw;
}
finally
{
Diagnostics.Flush();
}
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()

View File

@@ -10,8 +10,8 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\starter.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AssemblyVersion>1.2.0.4</AssemblyVersion>
<FileVersion>1.2.0.4</FileVersion>
<AssemblyVersion>1.3.0.0</AssemblyVersion>
<FileVersion>1.3.0.0</FileVersion>
<AssemblyName>Starter</AssemblyName>
<Company>eu07.pl</Company>
</PropertyGroup>

View File

@@ -1,15 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
Carbon theme for StarterNG.
Recreates the dark "Carbon" VCL look of the original Delphi Starter without
SukiUI: a flat charcoal palette, square bordered group panels, top tab-style
navigation and a full-width START button. Plain Avalonia controls only, so
every view's code-behind keeps working unchanged.
-->
<Styles.Resources>
<!-- ── Palette ──────────────────────────────────────────────────── -->
<Color x:Key="CarbonAppBgColor">#15171B</Color>
<Color x:Key="CarbonPanelBgColor">#1E2327</Color>
<Color x:Key="CarbonHeaderBgColor">#2A3036</Color>
@@ -25,14 +16,10 @@
<SolidColorBrush x:Key="CarbonBorderDark" Color="#0E0F12" />
<SolidColorBrush x:Key="CarbonFg" Color="#E6E8EA" />
<SolidColorBrush x:Key="CarbonFgDim" Color="#9098A0" />
<!-- Accents in MaSzyna green: a readable dark green for fills, bright green
for hover/highlights. -->
<SolidColorBrush x:Key="CarbonAccent" Color="#177f00" />
<SolidColorBrush x:Key="CarbonAccentHover" Color="#41c400" />
<SolidColorBrush x:Key="CarbonAccentBright" Color="#41c400" />
<!-- ── GroupBox (replaces SukiUI GroupBox) ──────────────────────────
Use on a HeaderedContentControl via Theme="{StaticResource GroupBox}". -->
<ControlTheme x:Key="GroupBox" TargetType="HeaderedContentControl">
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="Template">
@@ -45,10 +32,16 @@
Background="{StaticResource CarbonHeaderBg}"
BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="0,0,0,1"
Padding="8,5">
<TextBlock Text="{TemplateBinding Header}"
FontWeight="SemiBold"
Foreground="{StaticResource CarbonFg}" />
Padding="8,4">
<DockPanel LastChildFill="True">
<ContentPresenter DockPanel.Dock="Right"
Content="{TemplateBinding Tag}"
VerticalAlignment="Center" />
<TextBlock Text="{TemplateBinding Header}"
FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource CarbonFg}" />
</DockPanel>
</Border>
<ContentPresenter Content="{TemplateBinding Content}"
Margin="8" />
@@ -58,13 +51,12 @@
</Setter>
</ControlTheme>
<!-- ── Top navigation tab (RadioButton) ─────────────────────────────
Use via Theme="{StaticResource NavTab}". Single GroupName = one active. -->
<ControlTheme x:Key="NavTab" TargetType="RadioButton">
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinWidth" Value="150" />
<Setter Property="MinHeight" Value="50" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<ControlTemplate>
@@ -87,16 +79,98 @@
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type TabItem}" TargetType="TabItem">
<Setter Property="FontSize" Value="13" />
<Setter Property="MinHeight" Value="24" />
<Setter Property="Padding" Value="12,3" />
<Setter Property="Margin" Value="0,0,2,0" />
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="root" Background="Transparent"
BorderBrush="Transparent" BorderThickness="1,2,1,0"
CornerRadius="2,2,0,0"
Padding="{TemplateBinding Padding}">
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#root">
<Setter Property="Background" Value="{StaticResource CarbonHoverBg}" />
</Style>
<Style Selector="^:selected /template/ Border#root">
<Setter Property="Background" Value="{StaticResource CarbonHeaderBg}" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
</Style>
<Style Selector="^:selected">
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
</ControlTheme>
</Styles.Resources>
<!-- ── Window / page surfaces ─────────────────────────────────────────── -->
<Style Selector="Window">
<Setter Property="Background" Value="{StaticResource CarbonAppBg}" />
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="FontFamily" Value="Tahoma, Segoe UI, Inter" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="ComboBox">
<Setter Property="MinHeight" Value="24" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="8,1" />
<Setter Property="CornerRadius" Value="2" />
</Style>
<Style Selector="TextBox">
<Setter Property="MinHeight" Value="24" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="6,1" />
<Setter Property="CornerRadius" Value="2" />
</Style>
<Style Selector="NumericUpDown">
<Setter Property="MinHeight" Value="26" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="Button">
<Setter Property="MinHeight" Value="25" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="10,2" />
<Setter Property="CornerRadius" Value="2" />
</Style>
<Style Selector="CheckBox">
<Setter Property="MinHeight" Value="17" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="CheckBox.Checklist">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Height" Value="26" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="6,0,0,0" />
</Style>
<Style Selector="CheckBox.Checklist /template/ Grid">
<Setter Property="MinHeight" Value="0" />
</Style>
<Style Selector="CheckBox /template/ ContentPresenter">
<Setter Property="TextBlock.TextWrapping" Value="Wrap" />
</Style>
<Style Selector="RadioButton">
<Setter Property="MinHeight" Value="17" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="Label">
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="0,2" />
</Style>
<!-- ── START button (full-width, bold) ────────────────────────────────── -->
<Style Selector="Button.Start">
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
<Setter Property="Foreground" Value="White" />
@@ -118,7 +192,6 @@
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
</Style>
<!-- ── Accent button (Save etc.) ──────────────────────────────────────── -->
<Style Selector="Button.Accent /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
<Setter Property="TextBlock.Foreground" Value="White" />
@@ -128,7 +201,6 @@
<Setter Property="TextBlock.Foreground" Value="White" />
</Style>
<!-- ── Flat button (toolbar/secondary) ────────────────────────────────── -->
<Style Selector="Button.Flat">
<Setter Property="Background" Value="{StaticResource CarbonInputBg}" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonBorderLight}" />
@@ -136,11 +208,10 @@
<Setter Property="CornerRadius" Value="2" />
</Style>
<!-- ── Basic button (borderless icon buttons) ─────────────────────────── -->
<Style Selector="Button.Basic">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="6" />
<Setter Property="Padding" Value="4" />
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
</Style>
<Style Selector="Button.Basic /template/ ContentPresenter#PART_ContentPresenter">
@@ -151,7 +222,34 @@
<Setter Property="TextBlock.Foreground" Value="{StaticResource CarbonFg}" />
</Style>
<!-- ── Selection highlight in MaSzyna green (not the OS accent) ────────── -->
<Style Selector="StackPanel.Field">
<Setter Property="Spacing" Value="2" />
</Style>
<Style Selector="TextBlock.FieldLabel">
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.FieldValue">
<Setter Property="Foreground" Value="{StaticResource CarbonAccentBright}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="TextAlignment" Value="Right" />
<Setter Property="Margin" Value="8,0,0,0" />
</Style>
<Style Selector="TextBlock.FieldHint">
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontStyle" Value="Italic" />
</Style>
<Style Selector="Separator">
<Setter Property="Background" Value="{StaticResource CarbonBorderLight}" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="0,2" />
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
</Style>
@@ -171,36 +269,23 @@
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
</Style>
<!-- ── Tabs: smaller headers than Fluent's default, but tall enough that the
green selection pipe at the bottom doesn't touch the label. ─────────── -->
<Style Selector="TabItem">
<Setter Property="FontSize" Value="14" />
<Setter Property="MinHeight" Value="38" />
<Setter Property="Padding" Value="12,4,12,8" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- ── Compact list (tight rows that fill the available space) ─────────── -->
<!-- Transparent surface so the list blends into the GroupBox panel instead
of showing the Fluent theme's default light-grey ListBox background. -->
<Style Selector="ListBox.Compact">
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="ListBox.Compact ListBoxItem">
<Setter Property="Padding" Value="6,2" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="4,0" />
<Setter Property="MinHeight" Value="17" />
<Setter Property="FontSize" Value="13" />
</Style>
<!-- ── Compact tree (tight scenery rows) ──────────────────────────────── -->
<Style Selector="TreeView.Compact TreeViewItem">
<Setter Property="MinHeight" Value="22" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="19" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="TreeView.Compact TreeViewItem /template/ Border#PART_LayoutRoot">
<Setter Property="MinHeight" Value="22" />
<Setter Property="Padding" Value="4,1" />
<Setter Property="MinHeight" Value="19" />
<Setter Property="Padding" Value="3,0" />
</Style>
</Styles>

View File

@@ -0,0 +1,143 @@
using System;
using System.Diagnostics;
using System.IO;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Infrastructure;
namespace StarterNG.Views;
public static class AboutWindow
{
private const string RepositoryUrl = "https://github.com/maj00r/StarterNG";
private static readonly string[] ChangelogNames =
{
"changelog.txt", "starter/changelog.txt", "CHANGELOG.md"
};
public static Window Create()
{
var win = new Window
{
Title = App.Loc["AboutTitle"],
Width = 640,
Height = 460,
MinWidth = 480,
MinHeight = 320,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
var header = new StackPanel { Spacing = 2, Margin = new Avalonia.Thickness(20, 18, 20, 12) };
header.Children.Add(new TextBlock
{
Text = "Starter MaSzyna",
FontSize = 22,
FontWeight = FontWeight.Bold
});
header.Children.Add(new TextBlock
{
Text = $"{App.Loc["Version"]} {AppInfo.Version}",
Opacity = 0.8
});
header.Children.Add(new TextBlock
{
Text = App.Loc["AboutForSimulator"],
Opacity = 0.8
});
var repo = new Button
{
Content = RepositoryUrl,
Tag = RepositoryUrl,
Padding = new Avalonia.Thickness(0),
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Avalonia.Thickness(0, 6, 0, 10),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
repo.Classes.Add("Basic");
repo.Click += (_, _) => OpenUrl(RepositoryUrl);
header.Children.Add(repo);
var changelog = new TextBox
{
Text = ReadChangelog(),
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.NoWrap,
FontFamily = MonoFont.Family,
FontSize = 12,
Margin = new Avalonia.Thickness(20, 0, 20, 12)
};
ScrollViewer.SetHorizontalScrollBarVisibility(changelog, ScrollBarVisibility.Auto);
ScrollViewer.SetVerticalScrollBarVisibility(changelog, ScrollBarVisibility.Auto);
var close = new Button
{
Content = App.Loc["Close"],
MinWidth = 90,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
close.Classes.Add("Accent");
close.Click += (_, _) => win.Close();
var footer = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Avalonia.Thickness(20, 0, 20, 16),
Children = { close }
};
var root = new DockPanel();
DockPanel.SetDock(header, Dock.Top);
DockPanel.SetDock(footer, Dock.Bottom);
root.Children.Add(header);
root.Children.Add(footer);
var logLabel = new TextBlock
{
Text = App.Loc["AboutChangelog"],
Margin = new Avalonia.Thickness(20, 0, 20, 4),
Opacity = 0.8
};
DockPanel.SetDock(logLabel, Dock.Top);
root.Children.Add(logLabel);
root.Children.Add(changelog);
win.Content = root;
return win;
}
private static void OpenUrl(string url)
{
try
{
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log($"Open {url}", ex);
}
}
private static string ReadChangelog()
{
foreach (string name in ChangelogNames)
{
string path = Path.Combine(Directory.GetCurrentDirectory(), name);
if (!File.Exists(path)) continue;
try
{
return File.ReadAllText(path);
}
catch (Exception ex)
{
Diagnostics.Log($"Changelog {path}", ex);
}
}
return App.Loc["AboutNoChangelog"];
}
}

View File

@@ -0,0 +1,187 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Infrastructure;
using SettingsModel = StarterNG.Classes.Settings;
namespace StarterNG.Views;
public sealed class AdvancedSettingsWindow : Window
{
private readonly TextBox _log = new()
{
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.NoWrap,
FontFamily = MonoFont.Family,
FontSize = 12
};
private readonly CheckBox _compressTex = new();
private readonly CheckBox _mipmaps = new();
private readonly CheckBox _scaleSpeculars = new();
private readonly CheckBox _shaderGamma = new();
private readonly CheckBox _useGles = new();
private readonly CheckBox _convertModels = new();
private readonly CheckBox _resourceMove = new();
private readonly CheckBox _resourceSweep = new();
private readonly CheckBox _ignoreIrrelevant = new();
private readonly Slider _threads = new() { Minimum = 0, Maximum = 16, TickFrequency = 1, IsSnapToTickEnabled = true };
private readonly TextBlock _threadsValue = new() { VerticalAlignment = VerticalAlignment.Center, MinWidth = 24 };
public event Action<string>? ToolRequested;
public AdvancedSettingsWindow()
{
Title = App.Loc["AdvancedSettings"];
Width = 620;
Height = 470;
MinWidth = 480;
MinHeight = 360;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
_compressTex.Content = App.Loc["CompressTextures"];
_mipmaps.Content = App.Loc["ScreenMipmaps"];
_scaleSpeculars.Content = App.Loc["ScaleSpeculars"];
_shaderGamma.Content = App.Loc["ShaderGamma"];
_useGles.Content = App.Loc["UseGLES"];
_convertModels.Content = App.Loc["ExtendedModelConversion"];
_resourceMove.Content = App.Loc["GfxResourceMove"];
_resourceSweep.Content = App.Loc["GfxResourceSweep"];
_ignoreIrrelevant.Content = App.Loc["IgnoreIrrelevantTrains"];
_threads.PropertyChanged += (_, e) =>
{
if (e.Property == RangeBase.ValueProperty)
_threadsValue.Text = ((int)_threads.Value).ToString();
};
Load();
var tabs = new TabControl();
tabs.Items.Add(new TabItem { Header = App.Loc["AdvLogPage"], Content = BuildLogPage() });
tabs.Items.Add(new TabItem { Header = App.Loc["Advanced"], Content = BuildOptionsPage() });
tabs.Items.Add(new TabItem { Header = App.Loc["Tools"], Content = BuildToolsPage() });
var close = new Button
{
Content = App.Loc["Close"],
MinWidth = 100,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Avalonia.Thickness(0, 8, 0, 0),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
close.Classes.Add("Accent");
close.Click += (_, _) => { Store(); Close(); };
var root = new DockPanel { Margin = new Avalonia.Thickness(10) };
DockPanel.SetDock(close, Dock.Bottom);
root.Children.Add(close);
root.Children.Add(tabs);
Content = root;
}
private Control BuildLogPage()
{
_log.Text = Diagnostics.Text;
var clear = new Button
{
Content = App.Loc["AdvClearLog"],
Margin = new Avalonia.Thickness(0, 8, 0, 0),
HorizontalAlignment = HorizontalAlignment.Left,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
clear.Classes.Add("Flat");
clear.Click += (_, _) => { Diagnostics.Clear(); _log.Text = ""; };
ScrollViewer.SetHorizontalScrollBarVisibility(_log, ScrollBarVisibility.Auto);
ScrollViewer.SetVerticalScrollBarVisibility(_log, ScrollBarVisibility.Auto);
var panel = new DockPanel { Margin = new Avalonia.Thickness(8) };
DockPanel.SetDock(clear, Dock.Bottom);
panel.Children.Add(clear);
panel.Children.Add(_log);
return panel;
}
private Control BuildOptionsPage()
{
var sp = new StackPanel { Margin = new Avalonia.Thickness(10), Spacing = 8 };
foreach (var c in new Control[]
{
_compressTex, _scaleSpeculars, _useGles, _shaderGamma, _mipmaps,
_convertModels, _resourceMove, _resourceSweep, _ignoreIrrelevant
})
sp.Children.Add(c);
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Avalonia.Thickness(0, 6, 0, 0) };
row.Children.Add(new TextBlock
{
Text = App.Loc["TrainPhysicsThreads"],
VerticalAlignment = VerticalAlignment.Center
});
_threads.Width = 220;
row.Children.Add(_threads);
row.Children.Add(_threadsValue);
sp.Children.Add(row);
return new ScrollViewer { Content = sp, VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
}
private Control BuildToolsPage()
{
var sp = new StackPanel { Margin = new Avalonia.Thickness(10), Spacing = 8 };
Button Tool(string key, string tag)
{
var b = new Button
{
Content = App.Loc[key],
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
b.Classes.Add("Flat");
b.Click += (_, _) => ToolRequested?.Invoke(tag);
return b;
}
sp.Children.Add(Tool("AddAllCategory", "category"));
sp.Children.Add(Tool("AddUniqueMmd", "mmd"));
sp.Children.Add(Tool("RemoveAllTrains", "removeall"));
return sp;
}
private void Load()
{
var s = SettingsModel.Instance;
_compressTex.IsChecked = s.CompressTextures;
_mipmaps.IsChecked = s.ScreenMipmaps;
_scaleSpeculars.IsChecked = s.ScaleSpeculars;
_shaderGamma.IsChecked = s.ShaderGamma;
_useGles.IsChecked = s.UseGLES;
_convertModels.IsChecked = s.ExtendedModelConversion;
_resourceMove.IsChecked = s.GfxResourceMove;
_resourceSweep.IsChecked = s.GfxResourceSweep;
_ignoreIrrelevant.IsChecked = s.IgnoreIrrelevantTrains;
_threads.Value = s.TrainPhysicsThreads;
_threadsValue.Text = s.TrainPhysicsThreads.ToString();
}
private void Store()
{
var s = SettingsModel.Instance;
s.CompressTextures = _compressTex.IsChecked == true;
s.ScreenMipmaps = _mipmaps.IsChecked == true;
s.ScaleSpeculars = _scaleSpeculars.IsChecked == true;
s.ShaderGamma = _shaderGamma.IsChecked == true;
s.UseGLES = _useGles.IsChecked == true;
s.ExtendedModelConversion = _convertModels.IsChecked == true;
s.GfxResourceMove = _resourceMove.IsChecked == true;
s.GfxResourceSweep = _resourceSweep.IsChecked == true;
s.IgnoreIrrelevantTrains = _ignoreIrrelevant.IsChecked == true;
s.TrainPhysicsThreads = (int)_threads.Value;
}
}

View File

@@ -18,11 +18,43 @@ public static class ConsistContextMenu
Control owner,
Func<Trainset?> getTarget,
Action<List<Dynamic>> applyVehicles,
Action? removeAll = null)
Action? removeAll = null,
Func<Dynamic?>? getVehicle = null,
Action<Dynamic>? removeVehicle = null,
Action? randomOrder = null,
Action? randomTurn = null)
{
var menu = new ContextMenu();
menu.Opening += (_, _) => Populate(menu, owner, getTarget, applyVehicles, removeAll);
menu.Opening += (_, _) =>
{
Populate(menu, owner, getTarget, applyVehicles, removeAll);
if (randomOrder != null || randomTurn != null)
{
var schedule = new MenuItem { Header = App.Loc["ConsistSchedule"] };
if (randomOrder != null)
{
var mo = new MenuItem { Header = App.Loc["RandomizeOrder"] };
mo.Click += (_, _) => randomOrder();
schedule.Items.Add(mo);
}
if (randomTurn != null)
{
var mt = new MenuItem { Header = App.Loc["RandomizeOrientation"] };
mt.Click += (_, _) => randomTurn();
schedule.Items.Add(mt);
}
menu.Items.Add(schedule);
}
if (removeVehicle == null || getVehicle?.Invoke() is not { } v) return;
menu.Items.Add(new Separator());
var mi = new MenuItem { Header = App.Loc["RemoveVehicle"] };
mi.Click += (_, _) => removeVehicle(v);
menu.Items.Add(mi);
};
return menu;
}
@@ -50,6 +82,31 @@ public static class ConsistContextMenu
}
menu.Items.Add(load);
var append = new MenuItem { Header = App.Loc["WarehouseAppend"] };
if (presets.Count == 0)
{
append.Items.Add(new MenuItem { Header = App.Loc["PresetEmpty"], IsEnabled = false });
}
else
{
foreach (var preset in presets)
{
var entry = preset;
var mi = new MenuItem { Header = entry.Name };
mi.Click += (_, _) =>
{
var added = ConsistText.VehiclesFrom(entry.Entry);
if (added is null || added.Count == 0) return;
var merged = target is null
? added
: new List<Dynamic>(target.Vehicles).Concat(added).ToList();
applyVehicles(merged);
};
append.Items.Add(mi);
}
}
menu.Items.Add(append);
var sort = new MenuItem { Header = App.Loc["PresetSort"] };
var sortName = new MenuItem
{
@@ -148,7 +205,7 @@ public static class ConsistContextMenu
var nameBox = new TextBox
{
Watermark = App.Loc["PresetNamePrompt"],
PlaceholderText = App.Loc["PresetNamePrompt"],
Text = DefaultName(target),
MinWidth = 220
};

View File

@@ -7,162 +7,177 @@
x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
x:Class="StarterNG.Views.Depot">
<Grid Margin="0" ColumnSpacing="8" RowSpacing="8">
<Grid Margin="0" ColumnSpacing="3" RowSpacing="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="340" />
<ColumnDefinition Width="270" MinWidth="220" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- Consists list and the brakes/loads editor share the upper area,
the editor taking a bit more of it. -->
<RowDefinition Height="*" />
<RowDefinition Height="1.25*" MinHeight="250" />
<RowDefinition Height="190" />
<RowDefinition Height="*" MinHeight="230" />
<RowDefinition Height="275" MinHeight="260" />
<RowDefinition Height="175" MinHeight="165" />
</Grid.RowDefinitions>
<!-- Vehicle database browser (left, rows 0-1) -->
<Border Grid.RowSpan="2" Background="{StaticResource CarbonPanelBg}"
<Border Grid.Column="0" Grid.Row="0" Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Padding="8">
<DockPanel LastChildFill="True">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6" LastChildFill="True">
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto">
<DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True">
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="11"
VerticalAlignment="Center" Margin="8,0,0,0"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" />
<TextBox Name="searchBox" VerticalAlignment="Center"
Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
PlaceholderText="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
TextChanged="SearchBox_OnTextChanged" />
</DockPanel>
<Grid DockPanel.Dock="Top" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
<ComboBox Name="categoryCombo" Grid.Row="0" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" />
<Grid Grid.Row="1" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
<DockPanel Grid.Row="0" LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat"
Width="30" Margin="4,0,0,0" Cursor="Hand" Content="&#x1F50D;"
HorizontalContentAlignment="Center"
Click="TextureBaseButton_OnClick"
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Name="categoryCombo" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" />
</DockPanel>
<ComboBox Name="classCombo" Grid.Row="1" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid>
<!-- The arrow stays enabled with nothing selected - adding a whole
category needs no highlighted entry. -->
<DockPanel DockPanel.Dock="Bottom" Margin="0,8,0,0">
<Button Name="addMoreButton" DockPanel.Dock="Right" Classes="Flat Accent"
Width="28" Margin="4,0,0,0" Cursor="Hand" Content="&#x25BE;"
HorizontalContentAlignment="Center"
ToolTip.Tip="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}">
<Button.Flyout>
<MenuFlyout Placement="TopEdgeAlignedRight">
<MenuItem Click="AddAllCategoryButton_OnClick"
Header="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipAddAllCategory], Source={x:Static starterNg:App.Loc}}" />
<MenuItem Click="AddUniqueMmdButton_OnClick"
Header="{Binding [AddUniqueMmd], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipAddUniqueMmd], Source={x:Static starterNg:App.Loc}}" />
<Separator />
<MenuItem Click="TextureBaseButton_OnClick"
Header="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button Name="addVehicleButton" Classes="Flat Accent"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
IsEnabled="False" Cursor="Hand"
Click="AddVehicleButton_OnClick"
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Grid Name="browserDetails" DockPanel.Dock="Bottom" ColumnDefinitions="130,*"
ColumnSpacing="6" Margin="0,6,0,0" IsVisible="False">
<Border Name="miniPreviewPanel" Grid.Column="0" Height="66"
VerticalAlignment="Top"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="6" />
</Border>
<StackPanel Name="textureInfoPanel" Grid.Column="1" Spacing="1"
VerticalAlignment="Center" />
</Grid>
<Border Name="miniPreviewPanel" Grid.Row="3" Height="66"
Margin="0,6,0,0"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="6" />
</Border>
<!-- Flat list of vehicle entries (plain text, no expanders).
Selecting one shows its mini and enables Add; double-click
replaces the selected consist vehicle. -->
<ListBox Name="vehicleListBox" Classes="Compact" MinHeight="120"
<Button Name="addVehicleButton" Grid.Row="4" Classes="Flat Accent"
Margin="0,8,0,0" Cursor="Hand" IsEnabled="False"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Click="AddVehicleButton_OnClick"
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" />
<ListBox Name="vehicleListBox" Grid.Row="2" Classes="Compact"
SelectionChanged="VehicleListBox_OnSelectionChanged"
DoubleTapped="VehicleListBox_OnDoubleTapped"
PointerPressed="VehicleListBox_OnPointerPressed" />
</DockPanel>
</Grid>
</Border>
<!-- Consists present on the scenery selected in the Scenarios tab.
Selecting one loads it into the editor below; the header carries the
scenery name (see PopulateSceneryConsists). -->
<HeaderedContentControl Name="mapConsistsPanel" Grid.Column="1" Grid.Row="0"
Theme="{StaticResource GroupBox}">
<DockPanel LastChildFill="True">
<ListBox Name="sceneryConsistList" Classes="Compact"
SelectionChanged="SceneryConsistList_OnSelectionChanged"
KeyDown="SceneryConsistList_OnKeyDown" />
</DockPanel>
<TabControl>
<TabItem Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<Button Name="addToWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="AddToWarehouseButton_OnClick"
Content="{Binding [AddToWarehouse], Source={x:Static starterNg:App.Loc}}" />
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16"
Margin="2,6,0,0">
<CheckBox Name="depotDrivableOnlyCheck" FontSize="11"
IsCheckedChanged="ConsistFilter_OnChanged"
Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="depotShowAiCheck" FontSize="11"
IsCheckedChanged="ConsistFilter_OnChanged"
Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<ListBox Name="sceneryConsistList" Classes="Compact"
SelectionChanged="SceneryConsistList_OnSelectionChanged"
KeyDown="SceneryConsistList_OnKeyDown" />
</DockPanel>
</TabItem>
<TabItem Header="{Binding [WarehouseConsists], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<Button Name="removeFromWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="RemoveFromWarehouseButton_OnClick"
Content="{Binding [RemoveFromWarehouse], Source={x:Static starterNg:App.Loc}}" />
<ListBox Name="warehouseList" Classes="Compact"
DoubleTapped="WarehouseList_OnDoubleTapped" />
</DockPanel>
</TabItem>
</TabControl>
</HeaderedContentControl>
<!-- Brakes / Loads editor (middle-right). Header carries the vehicle
name (see UpdateDetails). -->
<HeaderedContentControl Name="selectedVehiclePanel" Grid.Column="1" Grid.Row="1"
<HeaderedContentControl Name="selectedVehiclePanel" Grid.Row="1" Grid.ColumnSpan="2"
Theme="{StaticResource GroupBox}">
<DockPanel LastChildFill="True">
<TabControl>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<Grid ColumnDefinitions="290*,3,380*,3,280*">
<TabControl Grid.Column="0">
<TabItem Header="{Binding [GeneralInfo], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True" Margin="4">
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="RemoveVehicleButton_OnClick"
Content="{Binding [RemoveVehicle], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="generalPanel" Spacing="6" />
</ScrollViewer>
</DockPanel>
</TabItem>
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="brakesPanel" Margin="4" Spacing="8" />
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Loads], Source={x:Static starterNg:App.Loc}}">
</TabControl>
<TabControl Grid.Column="2">
<TabItem Header="{Binding [Configuration], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="loadsPanel" Margin="4" Spacing="8" />
<StackPanel Name="loadsPanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="brakesPanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
</TabControl>
<TabControl Grid.Column="4">
<TabItem Header="{Binding [Coupling], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="couplerPanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="damagePanel" Margin="4" Spacing="8" />
<StackPanel Name="damagePanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</Grid>
</HeaderedContentControl>
<!-- Consist (full-width bottom, horizontally scrollable) -->
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<!-- Train totals + randomize tools -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="4">
<Button Name="randomTexturesButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomTexturesButton_OnClick"
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
<Button Name="randomOrderButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomOrderButton_OnClick"
Content="{Binding [RandomizeOrder], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeOrder], Source={x:Static starterNg:App.Loc}}" />
<Button Name="randomOrientButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomOrientButton_OnClick"
Content="{Binding [RandomizeOrientation], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeOrientation], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<TextBlock Name="trainStats" FontSize="12" Opacity="0.85"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<StackPanel Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" />
</DockPanel>
<Panel Name="consistDropTarget">
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel Name="consistStack" Orientation="Horizontal"
VerticalAlignment="Center" Spacing="0" />
<ScrollViewer Name="consistScroll"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Panel>
<StackPanel Name="consistStack" Orientation="Horizontal"
VerticalAlignment="Center" Spacing="0" />
<Canvas Name="consistOverlay" IsHitTestVisible="False" />
</Panel>
</ScrollViewer>
</Panel>
</DockPanel>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Infrastructure;
namespace StarterNG.Views;
public sealed class RulesWindow : Window
{
private readonly List<List<string>> _rules = new();
private readonly ListBox _ruleList = new() { Classes = { "Compact" } };
private readonly WrapPanel _members = new() { Orientation = Orientation.Horizontal };
private readonly ComboBox _category = new();
private readonly ComboBox _model = new();
private readonly VehicleDatabase _db = GameData.Instance.Vehicles;
public RulesWindow()
{
Title = App.Loc["RandomizeTexturesRules"];
Width = 720;
Height = 460;
MinWidth = 560;
MinHeight = 360;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
LoadRules();
BuildCategories();
_ruleList.SelectionChanged += (_, _) => ShowMembers();
var add = new Button
{
Content = App.Loc["RuleAdd"],
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
add.Classes.Add("Flat");
add.Click += (_, _) => AddToRule();
_category.SelectionChanged += (_, _) => FillModels();
var addRow = new Grid
{
Margin = new Avalonia.Thickness(0, 8, 0, 0),
ColumnDefinitions = new ColumnDefinitions("*,6,*,6,Auto")
};
_category.MinWidth = 0;
_model.MinWidth = 0;
_category.HorizontalAlignment = HorizontalAlignment.Stretch;
_model.HorizontalAlignment = HorizontalAlignment.Stretch;
Grid.SetColumn(_category, 0);
Grid.SetColumn(_model, 2);
Grid.SetColumn(add, 4);
addRow.Children.Add(_category);
addRow.Children.Add(_model);
addRow.Children.Add(add);
var save = new Button
{
Content = App.Loc["RandomizeTexturesRulesSave"],
MinWidth = 110,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
save.Classes.Add("Accent");
save.Click += async (_, _) => { if (await SaveRules()) Close(); };
var cancel = new Button
{
Content = App.Loc["Cancel"],
MinWidth = 90,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
cancel.Classes.Add("Flat");
cancel.Click += (_, _) => Close();
var footer = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
Margin = new Avalonia.Thickness(0, 10, 0, 0),
Children = { cancel, save }
};
var right = new DockPanel { LastChildFill = true };
DockPanel.SetDock(addRow, Dock.Bottom);
DockPanel.SetDock(footer, Dock.Bottom);
right.Children.Add(footer);
right.Children.Add(addRow);
right.Children.Add(new ScrollViewer
{
Content = _members,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto
});
var grid = new Grid
{
Margin = new Avalonia.Thickness(10),
ColumnDefinitions = new ColumnDefinitions("260,10,*")
};
Grid.SetColumn(_ruleList, 0);
Grid.SetColumn(right, 2);
grid.Children.Add(_ruleList);
grid.Children.Add(right);
Content = grid;
RefreshList();
}
private void LoadRules()
{
string path = TexRandomizer.RulesPath;
try
{
if (File.Exists(path))
foreach (string line in File.ReadAllLines(path))
{
var members = line.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
if (members.Count > 0)
_rules.Add(members);
}
}
catch (Exception ex)
{
Diagnostics.Log($"reguly.txt {path}", ex);
}
_rules.Add(new List<string>());
}
private async System.Threading.Tasks.Task<bool> SaveRules()
{
string path = TexRandomizer.RulesPath;
try
{
string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
File.WriteAllLines(path,
_rules.Where(r => r.Count > 0).Select(r => string.Join(",", r)));
return true;
}
catch (Exception ex)
{
await Diagnostics.ReportAsync(path, ex);
return false;
}
}
private void RefreshList()
{
int keep = _ruleList.SelectedIndex;
_ruleList.Items.Clear();
foreach (var rule in _rules)
_ruleList.Items.Add(new ListBoxItem
{
Content = new TextBlock
{
Text = rule.Count == 0 ? App.Loc["RuleNew"] : string.Join(" ", rule),
TextTrimming = TextTrimming.CharacterEllipsis,
Opacity = rule.Count == 0 ? 0.6 : 1
}
});
_ruleList.SelectedIndex = keep >= 0 && keep < _rules.Count ? keep : 0;
ShowMembers();
}
private void ShowMembers()
{
_members.Children.Clear();
int idx = _ruleList.SelectedIndex;
if (idx < 0 || idx >= _rules.Count) return;
foreach (string member in _rules[idx])
{
string name = member;
var btn = new Button
{
Content = name + " ✕",
FontSize = 12,
Margin = new Avalonia.Thickness(0, 0, 4, 4),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
btn.Classes.Add("Flat");
ToolTip.SetTip(btn, App.Loc["RuleRemove"]);
btn.Click += (_, _) =>
{
_rules[idx].Remove(name);
RefreshList();
};
_members.Children.Add(btn);
}
}
private void BuildCategories()
{
var cats = _db.Textures
.Select(t => t.ResolvedCategory)
.Where(c => !string.IsNullOrEmpty(c))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c, StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (string? c in cats)
_category.Items.Add(new ComboBoxItem { Content = c, Tag = c });
if (_category.Items.Count > 0)
_category.SelectedIndex = 0;
FillModels();
}
private void FillModels()
{
_model.Items.Clear();
string? cat = (_category.SelectedItem as ComboBoxItem)?.Tag as string;
var models = _db.Textures
.Where(t => cat == null ||
string.Equals(t.ResolvedCategory, cat, StringComparison.OrdinalIgnoreCase))
.Select(t => t.Model ?? "")
.Where(m => m.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(m => m, StringComparer.OrdinalIgnoreCase);
foreach (string m in models)
_model.Items.Add(m);
if (_model.Items.Count > 0)
_model.SelectedIndex = 0;
}
private void AddToRule()
{
int idx = _ruleList.SelectedIndex;
if (idx < 0 || idx >= _rules.Count) return;
if (_model.SelectedItem is not string model || model.Length == 0) return;
if (_rules[idx].Contains(model, StringComparer.OrdinalIgnoreCase))
return;
_rules[idx].Add(model);
_rules[idx].Sort(StringComparer.OrdinalIgnoreCase);
if (idx == _rules.Count - 1)
_rules.Add(new List<string>());
RefreshList();
}
}

View File

@@ -7,103 +7,104 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StarterNG.Views.Scenarios"
x:CompileBindings="True">
<Grid Margin="0" ColumnSpacing="8" RowSpacing="8">
<Grid Margin="0" ColumnSpacing="0" RowSpacing="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="280" />
<ColumnDefinition />
<ColumnDefinition Width="280" />
<ColumnDefinition Width="*" MinWidth="200" MaxWidth="460" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="3.2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="150" />
</Grid.RowDefinitions>
<!-- Scenery tree (left) -->
<HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}"
Classes="Compact"
Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<ToggleSwitch DockPanel.Dock="Bottom" Name="archivalSwitch"
Margin="2,4,0,0" FontSize="11"
IsCheckedChanged="ArchivalSwitch_OnChanged"
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
OffContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}" />
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8" Margin="2,4,0,0">
<Button x:Name="reloadButton" Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
Click="ReloadSceneries_OnClick"
Content="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
Click="OpenSceneryDir_OnClick"
Content="{Binding [OpenSceneryFolder], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<CheckBox DockPanel.Dock="Bottom" Name="archivalSwitch"
Margin="2,4,0,0" FontSize="11"
IsCheckedChanged="ArchivalSwitch_OnChanged"
Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" />
<TreeView Name="sceneryList" Classes="Compact"
SelectionChanged="SceneryList_OnSelectionChanged" />
SelectionChanged="SceneryList_OnSelectionChanged">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Click="ReloadSceneries_OnClick"
Header="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
<MenuItem Click="OpenSceneryDir_OnClick"
Header="{Binding [OpenSceneryFolder], Source={x:Static starterNg:App.Loc}}" />
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</DockPanel>
</HeaderedContentControl>
<!-- Centre: trainsets list (compact, fills) + description/weather/timetable tabs -->
<Grid Grid.Column="1" Grid.Row="0" RowSpacing="8">
<GridSplitter Grid.Column="1" Grid.Row="0" Width="6"
Background="Transparent" ResizeDirection="Columns" />
<Grid Grid.Column="2" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="3*" />
<RowDefinition Height="2*" MinHeight="90" />
<RowDefinition Height="4" />
<RowDefinition Height="3*" MinHeight="140" />
</Grid.RowDefinitions>
<HeaderedContentControl Grid.Row="0" Theme="{StaticResource GroupBox}"
Header="{Binding [Vehicles], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16"
Margin="2,4,0,0">
<CheckBox Name="showAiCheck" FontSize="11"
IsCheckedChanged="VehicleFilter_OnChanged"
Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="drivableOnlyCheck" FontSize="11"
IsCheckedChanged="VehicleFilter_OnChanged"
Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<ListBox Name="vehicleList" Classes="Compact"
SelectionChanged="VehicleList_OnSelectionChanged" />
</DockPanel>
Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
<HeaderedContentControl.Tag>
<Button Name="randomTexturesButton" Classes="Flat" Cursor="Hand"
FontSize="11" MinHeight="0" Padding="10,1"
Click="RandomTexturesButton_OnClick"
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
</HeaderedContentControl.Tag>
<ListBox Name="vehicleList" Classes="Compact"
SelectionChanged="VehicleList_OnSelectionChanged" />
</HeaderedContentControl>
<TabControl Grid.Row="1">
<GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch"
Background="Transparent" ResizeDirection="Rows" />
<TabControl Grid.Row="2">
<TabItem Header="{Binding [TabDescription], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="missionDescription" Margin="4" />
</ScrollViewer>
<Grid RowDefinitions="2*,6,*" Margin="4">
<Grid Grid.Row="0" ColumnDefinitions="*,3,225">
<HeaderedContentControl Grid.Column="0" Theme="{StaticResource GroupBox}"
Header="{Binding [SceneryDescription], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="sceneryDescription" />
</ScrollViewer>
</HeaderedContentControl>
<Border Grid.Column="2" ClipToBounds="True"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1">
<Image Name="sceneryImage" Stretch="Uniform" Margin="2" />
</Border>
</Grid>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="missionDescription" />
</ScrollViewer>
</Grid>
</TabItem>
<TabItem Header="{Binding [Weather], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="weatherForm" Margin="8" Spacing="8" IsEnabled="False">
<Grid ColumnDefinitions="130,*" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Name="weatherForm" Margin="8,6" Spacing="8" IsEnabled="False">
<Grid ColumnDefinitions="Auto,12,380,8,70"
RowDefinitions="Auto,6,Auto,14,Auto,Auto,8,Auto,Auto,14,Auto,14,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Time], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<TimePicker Name="weatherTime" ClockIdentifier="24HourClock"
MinuteIncrement="1" VerticalAlignment="Center"
SelectedTimeChanged="WeatherTime_OnChanged" />
<Button Name="weatherTimeNow" Classes="Flat" Cursor="Hand"
VerticalAlignment="Center" Padding="10,4"
Click="WeatherTimeNow_OnClick"
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
<Button Name="weatherRestore" Classes="Flat" Cursor="Hand"
VerticalAlignment="Center" Padding="10,4"
Click="WeatherRestore_OnClick"
Content="{Binding [RestoreWeather], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Season], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Grid.Row="1" Grid.Column="1" Name="weatherSeason" Width="180"
HorizontalAlignment="Left"
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Date], Source={x:Static starterNg:App.Loc}}" />
<Grid Grid.Row="0" Grid.Column="2" ColumnDefinitions="150,10,150,10,*">
<DatePicker Grid.Column="0" Name="weatherDate"
YearVisible="False" MinWidth="0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
SelectedDateChanged="WeatherDate_OnChanged" />
<ComboBox Grid.Column="2" Name="weatherSeason"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
SelectionChanged="WeatherSeason_OnChanged">
<ComboBoxItem Tag="0" Content="{Binding [SeasonNow], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="113" Content="{Binding [SeasonSpring], Source={x:Static starterNg:App.Loc}}" />
@@ -111,103 +112,121 @@
<ComboBoxItem Tag="297" Content="{Binding [SeasonAutumn], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="15" Content="{Binding [SeasonWinter], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Day], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Name="weatherDay" Width="140"
HorizontalAlignment="Left" Minimum="0" Maximum="366"
Increment="1" FormatString="0"
ValueChanged="WeatherDay_OnChanged" />
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Temperature], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Slider Name="weatherTemp" Minimum="-50" Maximum="50" Width="220"
TickFrequency="1" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Name="weatherTempLabel" VerticalAlignment="Center" MinWidth="48" />
</StackPanel>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Visibility], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Slider Name="weatherFog" Minimum="0" Maximum="10000" Width="220"
TickFrequency="250" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Name="weatherFogLabel" VerticalAlignment="Center" MinWidth="56" />
</StackPanel>
<TextBlock Grid.Row="5" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Overcast], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Grid.Row="5" Grid.Column="1" Name="weatherOvercast" Width="220"
HorizontalAlignment="Left"
SelectionChanged="Weather_OnSelectionChanged">
<ComboBoxItem Tag="-1.5" Content="{Binding [OvercastClear], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0" Content="{Binding [OvercastFair], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.3" Content="{Binding [OvercastLight], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.7" Content="{Binding [OvercastModerate], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [OvercastHeavy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.1" Content="{Binding [OvercastCloudy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.5" Content="{Binding [OvercastStorm], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<TextBlock Grid.Column="4" Name="weatherDayDate"
VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}" />
</Grid>
<TextBlock Name="weatherHint" Opacity="0.6" FontSize="11" TextWrapping="Wrap"
Text="{Binding [WeatherHint], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Time], Source={x:Static starterNg:App.Loc}}" />
<Grid Grid.Row="2" Grid.Column="2" ColumnDefinitions="150,8,*">
<TimePicker Grid.Column="0" Name="weatherTime"
ClockIdentifier="24HourClock" MinuteIncrement="1"
MinWidth="0" VerticalAlignment="Center"
HorizontalAlignment="Stretch"
SelectedTimeChanged="WeatherTime_OnChanged" />
<Button Grid.Column="2" Name="weatherTimeNow"
Classes="Flat" Cursor="Hand" VerticalAlignment="Center"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Click="WeatherTimeNow_OnClick"
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
</Grid>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Temperature], Source={x:Static starterNg:App.Loc}}" />
<Slider Grid.Row="4" Grid.Column="2" Name="weatherTemp"
Minimum="-50" Maximum="50"
TickFrequency="1" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Grid.Row="4" Grid.Column="4" Name="weatherTempLabel"
VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" />
<DockPanel Grid.Row="5" Grid.Column="2" Margin="0,-6,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11"
Foreground="{StaticResource CarbonFgDim}" Text="+50 °C" />
<TextBlock FontSize="11"
Foreground="{StaticResource CarbonFgDim}" Text="-50 °C" />
</DockPanel>
<TextBlock Grid.Row="7" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Visibility], Source={x:Static starterNg:App.Loc}}" />
<Slider Grid.Row="7" Grid.Column="2" Name="weatherFog"
Minimum="0" Maximum="1000"
TickFrequency="1" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Grid.Row="7" Grid.Column="4" Name="weatherFogLabel"
VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" />
<DockPanel Grid.Row="8" Grid.Column="2" Margin="0,-6,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [VisibilityGood], Source={x:Static starterNg:App.Loc}}" />
<TextBlock FontSize="11" Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [VisibilityBad], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<TextBlock Grid.Row="10" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Overcast], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Grid.Row="10" Grid.Column="2" Name="weatherOvercast"
HorizontalAlignment="Stretch"
SelectionChanged="Weather_OnSelectionChanged">
<ComboBoxItem Tag="random" Content="{Binding [OvercastRandom], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="-1.5" Content="{Binding [OvercastClear], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0" Content="{Binding [OvercastFair], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.3" Content="{Binding [OvercastLight], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.7" Content="{Binding [OvercastModerate], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [OvercastHeavy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.1" Content="{Binding [OvercastCloudy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.5" Content="{Binding [OvercastStorm], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<Border Grid.Row="11" Grid.Column="0" Grid.ColumnSpan="5"
Height="1" VerticalAlignment="Center"
Background="{StaticResource CarbonBorderLight}" />
<Button Grid.Row="12" Grid.Column="2" Grid.ColumnSpan="3"
Name="weatherRestore" Classes="Flat" Cursor="Hand"
HorizontalAlignment="Right" MinWidth="180"
HorizontalContentAlignment="Center"
Click="WeatherRestore_OnClick"
Content="{Binding [RestoreWeather], Source={x:Static starterNg:App.Loc}}" />
</Grid>
</StackPanel>
</TabItem>
<TabItem Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<TextBlock Name="timetableContent" Margin="4"
FontFamily="{x:Static infra:MonoFont.Family}"
FontSize="12" TextWrapping="NoWrap" />
</ScrollViewer>
</TabItem>
<TabItem Name="infoTab" Header="{Binding [TabInfo], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBox Name="faultListBox" Margin="4" IsReadOnly="True"
AcceptsReturn="True" TextWrapping="Wrap"
FontFamily="{x:Static infra:MonoFont.Family}" FontSize="12" />
</ScrollViewer>
<TabItem Name="timetableTab" ToolTip.ShowOnDisabled="True"
Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True" Margin="4">
<ScrollViewer DockPanel.Dock="Bottom" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled" MaxHeight="72">
<StackPanel Name="attachmentsPanel" Orientation="Horizontal" Spacing="6" />
</ScrollViewer>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<TextBlock Name="timetableContent"
FontFamily="{x:Static infra:MonoFont.Family}"
FontSize="12" TextWrapping="NoWrap" />
</ScrollViewer>
</DockPanel>
</TabItem>
</TabControl>
</Grid>
<!-- Right: scenery image (//$i) + scenery description (//$d) -->
<Grid Grid.Column="2" Grid.Row="0" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Border Grid.Row="0" ClipToBounds="True" VerticalAlignment="Top"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
Height="{Binding $self.Bounds.Width}">
<Image Name="sceneryImage" Stretch="Uniform" Margin="2" />
</Border>
<HeaderedContentControl Grid.Row="1" Theme="{StaticResource GroupBox}"
Header="{Binding [SceneryDescription], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<StackPanel Name="attachmentsPanel" DockPanel.Dock="Bottom" Spacing="2" Margin="0,6,0,0" />
<StackPanel Name="includesPanel" DockPanel.Dock="Bottom" Spacing="2" Margin="0,6,0,0" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="sceneryDescription" />
</ScrollViewer>
</DockPanel>
</HeaderedContentControl>
</Grid>
<!-- Consist preview (full-width bottom, horizontally scrollable) -->
<HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<TextBlock Name="trainStats" DockPanel.Dock="Top" FontSize="12"
Opacity="0.85" Margin="0,0,0,6" TextTrimming="CharacterEllipsis" />
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Name="consistStack" />
<StackPanel DockPanel.Dock="Top" Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" Margin="0,0,0,6" />
<ScrollViewer Name="consistScroll"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Panel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
Name="consistStack" />
<Canvas Name="consistOverlay" IsHitTestVisible="False" />
</Panel>
</ScrollViewer>
</DockPanel>
</HeaderedContentControl>

View File

@@ -8,7 +8,9 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
@@ -37,9 +39,7 @@ public partial class Scenarios : UserControl
Sceneries = GameData.Instance.Sceneries;
_loadingFilters = true;
showAiCheck.IsChecked = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
drivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
archivalSwitch.IsChecked = StarterNG.Classes.Settings.Instance.ShowArchivalSceneries;
archivalSwitch.IsChecked = !StarterNG.Classes.Settings.Instance.ShowArchivalSceneries;
_loadingFilters = false;
BuildSceneryTree();
@@ -63,7 +63,7 @@ public partial class Scenarios : UserControl
{
sceneryList.Items.Clear();
bool showArchival = archivalSwitch.IsChecked ?? true;
bool showArchival = archivalSwitch.IsChecked != true;
var groupNodes = new Dictionary<string, TreeViewItem>();
var topLevel = new List<TreeViewItem>();
@@ -213,15 +213,15 @@ public partial class Scenarios : UserControl
{
vehicleList.Items.Clear();
bool showAi = showAiCheck.IsChecked ?? true;
bool drivableOnly = drivableOnlyCheck.IsChecked ?? true;
bool showAi = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
bool drivableOnly = StarterNG.Classes.Settings.Instance.DrivableOnly;
for (int i = 0; i < scn.Trainsets.Count; i++)
{
var trainset = scn.Trainsets[i];
if (IsAiTrainset(trainset) && !showAi) continue;
if (trainset.Decor && drivableOnly) continue;
if (drivableOnly && !UData.StaffedTrain(trainset)) continue;
var listItem = new ListBoxItem
{
@@ -244,23 +244,11 @@ public partial class Scenarios : UserControl
!string.IsNullOrEmpty(trainset.Description) &&
trainset.Description.TrimStart().StartsWith("-", StringComparison.Ordinal);
private void VehicleFilter_OnChanged(object? sender, RoutedEventArgs e)
{
if (_loadingFilters) return;
StarterNG.Classes.Settings.Instance.ShowAiVehicles = showAiCheck.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.DrivableOnly = drivableOnlyCheck.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.Save();
if (sceneryList.SelectedItem is TreeViewItem { Tag: int tag } && tag < Sceneries.Count)
PopulateVehicleList(Sceneries[tag]);
}
private void ArchivalSwitch_OnChanged(object? sender, RoutedEventArgs e)
{
if (_loadingFilters) return;
StarterNG.Classes.Settings.Instance.ShowArchivalSceneries = archivalSwitch.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.ShowArchivalSceneries = archivalSwitch.IsChecked != true;
StarterNG.Classes.Settings.Instance.Save();
BuildSceneryTree();
@@ -329,9 +317,10 @@ public partial class Scenarios : UserControl
{
sceneryImage.Source = new Bitmap(imagePath);
}
catch
catch (Exception ex)
{
sceneryImage.Source = null;
StarterNG.Infrastructure.Diagnostics.Log($"Scenery image {imagePath}", ex);
}
}
else
@@ -340,7 +329,6 @@ public partial class Scenarios : UserControl
}
PopulateAttachments(scenery);
PopulateIncludes(scenery);
PopulateFaults(scenery);
}
@@ -368,8 +356,8 @@ public partial class Scenarios : UserControl
lines.Add($"# unknown loose texture: {v.Name} ({v.SkinFile})");
}
faultListBox.Text = string.Join(Environment.NewLine, lines);
infoTab.IsVisible = lines.Count > 0 || scenery.Includes.Any(i => i.Kind != 2);
if (lines.Count > 0)
StarterNG.Infrastructure.Diagnostics.Log(lines);
}
private void PopulateAttachments(Scenery scenery)
@@ -392,38 +380,6 @@ public partial class Scenarios : UserControl
}
}
private void PopulateIncludes(Scenery scenery)
{
includesPanel.Children.Clear();
var opts = scenery.Includes.Where(i => i.Kind != 2).ToList();
if (opts.Count == 0) return;
includesPanel.Children.Add(new TextBlock
{
Text = App.Loc["OptionalIncludes"],
FontWeight = Avalonia.Media.FontWeight.Bold,
FontSize = 12,
Margin = new Thickness(0, 0, 0, 2)
});
foreach (var inc in opts)
{
var check = new CheckBox
{
Content = $"{inc.Desc} ({inc.FilePath})",
IsChecked = inc.Selected,
FontSize = 11,
Tag = inc
};
check.IsCheckedChanged += (_, _) =>
{
if (check.Tag is SceneryInclude s)
s.Selected = check.IsChecked == true;
};
includesPanel.Children.Add(check);
}
}
private static void OpenAttachment(string relativePath)
{
string full = Path.GetFullPath(relativePath);
@@ -453,9 +409,12 @@ public partial class Scenarios : UserControl
RefreshSelectedConsist();
}
private bool _reloading;
private async void ReloadSceneries_OnClick(object? sender, RoutedEventArgs e)
{
reloadButton.IsEnabled = false;
if (_reloading) return;
_reloading = true;
try
{
string? selected = AppState.Instance.CurrentScenery?.Path;
@@ -471,22 +430,94 @@ public partial class Scenarios : UserControl
}
finally
{
reloadButton.IsEnabled = true;
_reloading = false;
}
}
public event Action<Scenery>? RandomizeTexturesRequested;
private void RandomTexturesButton_OnClick(object? sender, RoutedEventArgs e)
{
if (AppState.Instance.CurrentScenery is { } scenery)
RandomizeTexturesRequested?.Invoke(scenery);
}
public void RefreshConsistView()
{
RefreshVehicleLabels();
if (AppState.Instance.CurrentTrainset is { } trainset)
ShowConsist(trainset);
}
private static readonly Avalonia.Media.IBrush SelectedVehicleBrush =
new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#41C400"));
private static readonly Avalonia.Media.IBrush MissingMiniBrush =
new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#22808080"));
private static Control MissingMiniBox(string? label, int height) => new Border
{
Height = height,
Width = height * 2,
Background = MissingMiniBrush,
CornerRadius = new Avalonia.CornerRadius(4),
Child = new TextBlock
{
Text = label,
FontSize = 10,
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = Avalonia.Media.TextAlignment.Center
}
};
private readonly Dictionary<string, Bitmap?> _miniCache = new();
private Bitmap? LoadMini(string? name, int height)
{
string key = $"{name}@{height}";
if (_miniCache.TryGetValue(key, out var cached))
return cached;
Bitmap? bmp = null;
string? path = VehicleDatabase.MiniPath(name);
if (path != null)
{
try
{
using var fs = File.OpenRead(path);
bmp = Bitmap.DecodeToHeight(fs, height);
}
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.Log($"Mini {path}", ex);
}
}
_miniCache[key] = bmp;
return bmp;
}
private void OpenSceneryDir_OnClick(object? sender, RoutedEventArgs e)
{
var scn = AppState.Instance.CurrentScenery;
string dir = scn != null
? (Path.GetDirectoryName(Path.GetFullPath(scn.Path)) ?? "scenery")
: Path.GetFullPath("scenery");
if (!Directory.Exists(dir)) return;
if (!Directory.Exists(dir))
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(string.Format(App.Loc["FaultFileNotFound"], dir));
return;
}
try
{
Process.Start(new ProcessStartInfo(dir) { UseShellExecute = true });
}
catch { }
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
$"{dir}{Environment.NewLine}{Environment.NewLine}{App.Loc["FaultDetail"]} {ex.Message}");
}
}
private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
@@ -518,11 +549,13 @@ public partial class Scenarios : UserControl
{
weatherForm.IsEnabled = true;
weatherTime.SelectedTime = ParseTime(scenery.ScenarioTimeOverride);
weatherDay.Value = scenery.Day;
weatherDate.SelectedDate = DateOfDayOfYear(scenery.Day);
weatherTemp.Value = scenery.Temperature;
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
SelectByTag(weatherSeason, scenery.Day.ToString());
SelectByTag(weatherOvercast, scenery.Overcast.ToString(CultureInfo.InvariantCulture));
weatherFog.Value = SliderFromFog(scenery.FogEnd);
SelectByTag(weatherSeason, scenery.Day.ToString(), excludeFromNearest: "0");
SelectByTag(weatherOvercast, scenery.OvercastRandom
? RandomTag
: scenery.Overcast.ToString(CultureInfo.InvariantCulture));
ApplyTodayPreview(scenery.Day == 0);
UpdateWeatherLabels();
@@ -542,12 +575,16 @@ public partial class Scenarios : UserControl
if (weatherTime.SelectedTime is { } t)
scn.ScenarioTimeOverride = $"{t.Hours:D2}:{t.Minutes:D2}";
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
scn.Day = _todaySeason ? 0 : SelectedDayOfYear();
scn.Temperature = weatherTemp.Value;
scn.FogEnd = (int)weatherFog.Value;
if ((weatherOvercast.SelectedItem as ComboBoxItem)?.Tag is string ov &&
double.TryParse(ov, System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out double oc))
scn.Overcast = oc;
scn.FogEnd = FogFromSlider(weatherFog.Value);
if ((weatherOvercast.SelectedItem as ComboBoxItem)?.Tag is string ov)
{
scn.OvercastRandom = ov == RandomTag;
if (!scn.OvercastRandom &&
double.TryParse(ov, NumberStyles.Float, CultureInfo.InvariantCulture, out double oc))
scn.Overcast = oc;
}
scn.WeatherDirty = true;
UpdateWeatherLabels();
@@ -556,18 +593,85 @@ public partial class Scenarios : UserControl
private void UpdateWeatherLabels()
{
weatherTempLabel.Text = $"{(int)weatherTemp.Value} °C";
weatherFogLabel.Text = $"{(int)weatherFog.Value} m";
weatherFogLabel.Text = FormatFog(FogFromSlider(weatherFog.Value));
weatherDayDate.Text = string.Format(LanguageCulture, App.Loc["DayOfYear"], SelectedDayOfYear());
weatherRestore.IsEnabled = _currentScenery?.WeatherChanged ?? false;
}
private static void SelectByTag(ComboBox combo, string tag)
private static int FogFromSlider(double position)
{
double metres = Scenery.FogMin *
System.Math.Pow((double)Scenery.FogMax / Scenery.FogMin, position / 1000.0);
int step = metres < 100 ? 5 : metres < 1000 ? 10 : 100;
return System.Math.Clamp((int)(System.Math.Round(metres / step) * step),
Scenery.FogMin, Scenery.FogMax);
}
private static double SliderFromFog(int metres)
{
double clamped = System.Math.Clamp(metres, Scenery.FogMin, Scenery.FogMax);
return 1000.0 * System.Math.Log(clamped / Scenery.FogMin) /
System.Math.Log((double)Scenery.FogMax / Scenery.FogMin);
}
private static string FormatFog(int metres) =>
metres < 1000
? $"{metres} m"
: string.Format(LanguageCulture, "{0:0.#} km", metres / 1000.0);
private static CultureInfo LanguageCulture
{
get
{
string code = App.Loc.CurrentLangCode;
if (code is "en" or "")
return CultureInfo.InvariantCulture;
try
{
return CultureInfo.GetCultureInfo(code == "cz" ? "cs" : code);
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
}
private const string RandomTag = "random";
private static void SelectByTag(ComboBox combo, string tag, string? excludeFromNearest = null)
{
ComboBoxItem? first = null;
ComboBoxItem? nearest = null;
double bestDistance = double.MaxValue;
bool haveValue = double.TryParse(tag, NumberStyles.Float, CultureInfo.InvariantCulture,
out double value);
foreach (var obj in combo.Items)
if (obj is ComboBoxItem item && (item.Tag as string) == tag)
{
if (obj is not ComboBoxItem item)
continue;
first ??= item;
if ((item.Tag as string) == tag)
{
combo.SelectedItem = item;
return;
}
combo.SelectedItem = null;
if (haveValue && item.Tag is string itemTag && itemTag != excludeFromNearest &&
double.TryParse(itemTag, NumberStyles.Float, CultureInfo.InvariantCulture,
out double itemValue))
{
double distance = System.Math.Abs(itemValue - value);
if (distance < bestDistance)
{
bestDistance = distance;
nearest = item;
}
}
}
combo.SelectedItem = nearest ?? first;
}
private void WeatherTime_OnChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) => CaptureWeather();
@@ -600,12 +704,11 @@ public partial class Scenarios : UserControl
private void ApplyTodayPreview(bool today)
{
_todaySeason = today;
weatherDay.IsEnabled = !today;
if (today)
{
bool prev = _loadingWeather;
_loadingWeather = true;
weatherDay.Value = DateTime.Now.DayOfYear;
weatherDate.SelectedDate = DateOfDayOfYear(0);
_loadingWeather = prev;
}
}
@@ -614,7 +717,28 @@ public partial class Scenarios : UserControl
private void Weather_OnSliderChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) => CaptureWeather();
private void WeatherDay_OnChanged(object? sender, NumericUpDownValueChangedEventArgs e) => CaptureWeather();
private void WeatherDate_OnChanged(object? sender, DatePickerSelectedValueChangedEventArgs e)
{
if (!_loadingWeather)
{
_todaySeason = false;
_loadingWeather = true;
SelectByTag(weatherSeason, SelectedDayOfYear().ToString(), excludeFromNearest: "0");
_loadingWeather = false;
}
CaptureWeather();
}
private static DateTimeOffset DateOfDayOfYear(int day)
{
int year = DateTime.Now.Year;
int last = DateTime.IsLeapYear(year) ? 366 : 365;
int d = System.Math.Clamp(day <= 0 ? DateTime.Now.DayOfYear : day, 1, last);
return new DateTimeOffset(new DateTime(year, 1, 1).AddDays(d - 1));
}
private int SelectedDayOfYear() =>
weatherDate.SelectedDate is { } date ? date.DayOfYear : DateTime.Now.DayOfYear;
private void WeatherSeason_OnChanged(object? sender, SelectionChangedEventArgs e)
{
@@ -631,7 +755,7 @@ public partial class Scenarios : UserControl
{
ApplyTodayPreview(false);
_loadingWeather = true;
weatherDay.Value = day;
weatherDate.SelectedDate = DateOfDayOfYear(day);
_loadingWeather = false;
}
}
@@ -644,19 +768,33 @@ public partial class Scenarios : UserControl
if (path is null)
{
timetableContent.Text = App.Loc["NoTimetable"];
UpdateTimetableTab(false);
return;
}
try
{
timetableContent.Text = File.ReadAllText(path, Encoding.GetEncoding(1250));
UpdateTimetableTab(true);
}
catch
{
timetableContent.Text = App.Loc["NoTimetable"];
UpdateTimetableTab(false);
}
}
private void UpdateTimetableTab(bool hasTimetable)
{
bool usable = hasTimetable || attachmentsPanel.Children.Count > 0;
timetableTab.IsEnabled = usable;
ToolTip.SetTip(timetableTab, usable ? null : App.Loc["NoTimetable"]);
if (!usable && timetableTab.IsSelected && timetableTab.Parent is TabControl tabs)
tabs.SelectedIndex = 0;
}
private static string? ResolveTimetablePath(Scenery scenery, string? name)
{
if (string.IsNullOrWhiteSpace(name) ||
@@ -677,27 +815,127 @@ public partial class Scenarios : UserControl
return candidates.FirstOrDefault(File.Exists);
}
private const double ConsistDragThreshold = 6;
private Trainset? _stripTrainset;
private int _pressIndex = -1;
private Point _pressPoint;
private bool _dragging;
private bool _stripWired;
private int _dragBlockStart = -1;
private int _dragBlockLength;
private double _dragBlockWidth = 32;
private double _grabOffset;
private Control? _carried;
private readonly List<int> _reducedToVehicle = new();
private readonly List<int> _unitStarts = new();
private readonly Infrastructure.DropGap _dropGap = new();
private readonly Infrastructure.DropIndexTracker _dropTracker = new();
private readonly Infrastructure.EdgeScroller _edgeScroller = new();
private void ShowConsist(Trainset trainset)
{
consistStack.Children.Clear();
_stripTrainset = trainset;
_dropGap.Forget();
if (!_stripWired)
{
_stripWired = true;
consistStack.PointerMoved += Strip_OnPointerMoved;
consistStack.PointerReleased += Strip_OnPointerReleased;
consistStack.PointerCaptureLost += (_, _) => CancelConsistDrag();
}
ContextMenu VehicleMenu(Dynamic vehicle) => ConsistContextMenu.Build(
consistStack,
() => trainset,
vehicles =>
{
trainset.Vehicles = vehicles;
RefreshVehicleLabels();
ShowConsist(trainset);
UpdateTrainStats(trainset);
},
removeAll: null,
getVehicle: () => vehicle,
removeVehicle: v =>
{
int gap = trainset.Vehicles.IndexOf(v);
trainset.Vehicles.Remove(v);
bool wasPicked = !string.IsNullOrEmpty(v.Name) &&
string.Equals(v.Name, AppState.Instance.StartingVehicleName,
StringComparison.OrdinalIgnoreCase);
if (wasPicked && trainset.Vehicles.Count > 0)
{
int next = Math.Clamp(gap, 0, trainset.Vehicles.Count - 1);
AppState.Instance.StartingVehicleName = trainset.Vehicles[next].Name;
AppState.Instance.NotifyChanged();
}
RefreshVehicleLabels();
ShowConsist(trainset);
UpdateTrainStats(trainset);
});
var db = GameData.Instance.Vehicles;
foreach (var train in trainset.Vehicles)
{
string miniName = db.MiniForSkin(train.SkinFile) ?? train.SkinFile;
string? path = VehicleDatabase.MiniPath(miniName);
if (path is null)
continue;
if (!VehicleDatabase.HasMini(miniName) && VehicleDatabase.HasMini(train.SkinFile))
miniName = train.SkinFile;
int thumbH = StarterNG.Classes.Settings.Instance.LargeThumbnails ? 64 : 32;
consistStack.Children.Add(new Image
var bmp = LoadMini(miniName, thumbH);
Control visual = bmp is not null
? new Image
{
Source = bmp,
Height = thumbH,
Stretch = Avalonia.Media.Stretch.Uniform,
Margin = new Thickness(0)
}
: MissingMiniBox(train.SkinFile, thumbH);
bool selected = !string.IsNullOrEmpty(train.Name) &&
string.Equals(train.Name, AppState.Instance.StartingVehicleName,
StringComparison.OrdinalIgnoreCase);
var slot = new Border
{
Source = new Bitmap(path),
Height = thumbH,
Stretch = Avalonia.Media.Stretch.Uniform,
Margin = new Thickness(0)
});
Transitions = new Transitions
{
new DoubleTransition
{
Property = OpacityProperty,
Duration = TimeSpan.FromMilliseconds(120)
}
},
Child = visual,
Padding = new Thickness(0),
BorderThickness = new Thickness(0, 2),
BorderBrush = selected ? SelectedVehicleBrush : Avalonia.Media.Brushes.Transparent,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand),
ContextMenu = VehicleMenu(train)
};
ToolTip.SetTip(slot, train.Name);
int slotIndex = consistStack.Children.Count;
slot.PointerPressed += (_, ev) =>
{
if (!ev.GetCurrentPoint(slot).Properties.IsLeftButtonPressed) return;
_pressIndex = slotIndex;
_pressPoint = ev.GetPosition(consistStack);
_dragging = false;
};
consistStack.Children.Add(slot);
}
string desc = trainset.Description ?? "";
if (desc.TrimStart().StartsWith('-'))
@@ -706,22 +944,256 @@ public partial class Scenarios : UserControl
UpdateTrainStats(trainset);
}
private void Strip_OnPointerMoved(object? sender, PointerEventArgs e)
{
var point = e.GetPosition(consistStack);
if (!_dragging)
{
if (_pressIndex < 0 || !e.GetCurrentPoint(consistStack).Properties.IsLeftButtonPressed)
return;
var delta = point - _pressPoint;
if (Math.Abs(delta.X) < ConsistDragThreshold && Math.Abs(delta.Y) < ConsistDragThreshold)
return;
BeginConsistDrag(e);
if (!_dragging) return;
}
Canvas.SetLeft(_carried!, point.X - _grabOffset);
double pointerX = e.GetPosition(consistScroll).X;
if (_dropTracker.Update(pointerX + consistScroll.Offset.X))
{
int reduced = ReducedTarget();
_dropGap.Show(consistStack,
reduced < _reducedToVehicle.Count
? _reducedToVehicle[reduced]
: consistStack.Children.Count,
_dragBlockWidth);
}
_edgeScroller.Update(consistScroll, pointerX);
}
private void Strip_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_dragging)
{
int index = _pressIndex;
_pressIndex = -1;
if (e.InitialPressMouseButton != MouseButton.Left ||
index < 0 || _stripTrainset is not { } clicked ||
index >= clicked.Vehicles.Count)
return;
AppState.Instance.StartingVehicleName = clicked.Vehicles[index].Name;
AppState.Instance.NotifyChanged();
ShowConsist(clicked);
return;
}
int target = _dropTracker.Index >= 0 ? ReducedTarget() : -1;
int start = _dragBlockStart;
int length = _dragBlockLength;
var trainset = _stripTrainset;
EndConsistDrag();
if (trainset != null && start >= 0 && target >= 0)
MoveConsistBlock(trainset, start, length, target);
}
private void BeginConsistDrag(PointerEventArgs e)
{
if (_stripTrainset is not { } trainset || _pressIndex < 0 ||
_pressIndex >= trainset.Vehicles.Count)
return;
_dragBlockStart = SetStart(trainset.Vehicles, _pressIndex);
_dragBlockLength = SetLength(trainset.Vehicles, _dragBlockStart);
if (_dragBlockStart + _dragBlockLength > consistStack.Children.Count)
return;
var first = consistStack.Children[_dragBlockStart];
var blockBounds = first.Bounds;
_dragBlockWidth = 0;
for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++)
_dragBlockWidth += consistStack.Children[i].Bounds.Width;
_dragBlockWidth = Math.Max(_dragBlockWidth, 16);
_grabOffset = Math.Clamp(_pressPoint.X - blockBounds.Left, 0, _dragBlockWidth);
CaptureDropTargets(trainset);
_carried = BuildCarriedVisual(blockBounds.Height);
Canvas.SetTop(_carried, blockBounds.Top);
Canvas.SetLeft(_carried, blockBounds.Left);
consistOverlay.Children.Add(_carried);
for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++)
consistStack.Children[i].IsVisible = false;
_dragging = true;
e.Pointer.Capture(consistStack);
}
private void CaptureDropTargets(Trainset trainset)
{
var vehicles = trainset.Vehicles;
_reducedToVehicle.Clear();
for (int i = 0; i < vehicles.Count && i < consistStack.Children.Count; i++)
if (i < _dragBlockStart || i >= _dragBlockStart + _dragBlockLength)
_reducedToVehicle.Add(i);
_unitStarts.Clear();
var midpoints = new List<double>();
int r = 0;
while (r < _reducedToVehicle.Count)
{
int length = 1;
while (r + length < _reducedToVehicle.Count &&
_reducedToVehicle[r + length] == _reducedToVehicle[r + length - 1] + 1 &&
HoldsNext(vehicles[_reducedToVehicle[r + length - 1]]))
length++;
double left = ChildEdge(_reducedToVehicle[r], right: false);
double right = ChildEdge(_reducedToVehicle[r + length - 1], right: true);
midpoints.Add((left + right) / 2);
_unitStarts.Add(r);
r += length;
}
_dropTracker.Capture(midpoints);
}
private double ChildEdge(int childIndex, bool right)
{
var bounds = consistStack.Children[childIndex].Bounds;
double edge = right ? bounds.Right : bounds.Left;
return childIndex > _dragBlockStart ? edge - _dragBlockWidth : edge;
}
private int ReducedTarget()
{
int unit = _dropTracker.Index;
if (unit < 0) return -1;
return unit < _unitStarts.Count ? _unitStarts[unit] : _reducedToVehicle.Count;
}
private Control BuildCarriedVisual(double height)
{
var panel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal };
for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++)
{
var source = consistStack.Children[i];
Control copy = source is Border { Child: Image image }
? new Image { Source = image.Source, Height = image.Height, Stretch = image.Stretch }
: new Border
{
Width = source.Bounds.Width,
Height = source.Bounds.Height,
Background = MissingMiniBrush
};
panel.Children.Add(copy);
}
return new Border
{
Child = panel,
Height = height,
Opacity = 0.85,
BorderThickness = new Thickness(0, 2),
BorderBrush = SelectedVehicleBrush
};
}
private void CancelConsistDrag()
{
if (!_dragging) return;
EndConsistDrag();
}
private void EndConsistDrag()
{
for (int i = _dragBlockStart; i >= 0 && i < _dragBlockStart + _dragBlockLength; i++)
if (i < consistStack.Children.Count)
consistStack.Children[i].IsVisible = true;
if (_carried != null)
consistOverlay.Children.Remove(_carried);
_carried = null;
_dropTracker.Reset();
_edgeScroller.Stop();
_dropGap.Hide(consistStack);
_dragBlockStart = -1;
_dragBlockLength = 0;
_pressIndex = -1;
_dragging = false;
}
private void MoveConsistBlock(Trainset trainset, int start, int length, int target)
{
var vehicles = trainset.Vehicles;
if (start < 0 || length <= 0 || start + length > vehicles.Count)
return;
var block = vehicles.GetRange(start, length);
vehicles.RemoveRange(start, length);
target = Math.Clamp(target, 0, vehicles.Count);
if (target == start)
{
vehicles.InsertRange(start, block);
return;
}
vehicles.InsertRange(target, block);
RefreshVehicleLabels();
ShowConsist(trainset);
UpdateTrainStats(trainset);
}
private static bool HoldsNext(Dynamic car) =>
car.Coupling.Has(Coupling.WorkshopLock) || car.Coupling.Locked;
private static int SetStart(List<Dynamic> vehicles, int index)
{
while (index > 0 && HoldsNext(vehicles[index - 1]))
index--;
return index;
}
private static int SetLength(List<Dynamic> vehicles, int start)
{
int length = 1;
while (start + length < vehicles.Count && HoldsNext(vehicles[start + length - 1]))
length++;
return length;
}
private void UpdateTrainStats(Trainset? trainset)
{
if (trainset is null)
{
trainStats.Text = "";
trainStats.Children.Clear();
return;
}
var db = GameData.Instance.Vehicles;
trainStats.Text = TrainsetDisplay.FormatStats(
StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields(
trainset,
car => Physics.For(car.DataFolder, db.TextureForSkin(car.SkinFile)?.Model)
?? Physics.For(car.DataFolder, car.MmdFile)
?? Physics.For(car.DataFolder, car.SkinFile),
App.Loc["Length"], App.Loc["Mass"], App.Loc["VehicleCount"], App.Loc["Track"],
car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory);
car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory));
}
private void RefreshSelectedConsist()

View File

@@ -7,377 +7,551 @@
x:Class="StarterNG.Views.Settings"
x:CompileBindings="True">
<DockPanel>
<!-- Save / reset bar -->
<Border DockPanel.Dock="Bottom" Padding="16,10" Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Right">
<TextBlock x:Name="SaveStatus" VerticalAlignment="Center" Opacity="0.8" Text="" />
<Button x:Name="ResetButton" Classes="Flat" Click="ResetButton_OnClick"
Content="{Binding [Reset], Source={x:Static starterNg:App.Loc}}" />
<Button x:Name="SaveButton" Classes="Flat Accent" Click="SaveButton_OnClick"
Content="{Binding [Save], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center" Opacity="0.6" FontSize="11"
Text="{Binding [SettingsPath], Source={x:Static starterNg:App.Loc}}" />
<TextBlock x:Name="SettingsPathText" VerticalAlignment="Center" Opacity="0.8" FontSize="11"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12" HorizontalAlignment="Right">
<TextBlock x:Name="SaveStatus" VerticalAlignment="Center" Opacity="0.8" Text="" />
<Button x:Name="AboutButton" Classes="Flat" Click="AboutButton_OnClick"
Content="{Binding [About], Source={x:Static starterNg:App.Loc}}" />
<Button x:Name="ResetButton" Classes="Flat" Click="ResetButton_OnClick"
Content="{Binding [Reset], Source={x:Static starterNg:App.Loc}}" />
<Button x:Name="SaveButton" Classes="Flat Accent" Click="SaveButton_OnClick"
Content="{Binding [Save], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</Grid>
</Border>
<TabControl TabStripPlacement="Left">
<!-- General -->
<TabControl TabStripPlacement="Top">
<TabItem Header="{Binding [General], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<Label Content="{Binding [Language], Source={x:Static starterNg:App.Loc}}" VerticalAlignment="Center" FontSize="14" />
<!-- Filled from the lang/ folder in the constructor. -->
<ComboBox x:Name="ChangeLanguageCb" MinWidth="350" MaxWidth="350"
SelectionChanged="LanguageComboBox_OnSelectionChanged" />
</StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<Grid ColumnDefinitions="*,10,*,10,200" Margin="10">
<CheckBox x:Name="FullscreenCb" Content="{Binding [Fullscreen], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseInactiveCb" Content="{Binding [PauseSimInactive], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseStartCb" Content="{Binding [PauseSimStart], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Column="0" Spacing="10">
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [General], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [Language], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ChangeLanguageCb" HorizontalAlignment="Stretch"
SelectionChanged="LanguageComboBox_OnSelectionChanged" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [CursorSensitivity], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="CursorSensitivitySlider" Minimum="1" Maximum="5" TickFrequency="1" MinWidth="200" MaxWidth="200"
IsSnapToTickEnabled="True" Value="3" />
</StackPanel>
<CheckBox x:Name="FullscreenCb" Content="{Binding [Fullscreen], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseInactiveCb" Content="{Binding [PauseSimInactive], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseStartCb" Content="{Binding [PauseSimStart], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MouseHorInvertCb" Content="{Binding [MouseHorInvert], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MouseVertInvertCb" Content="{Binding [MouseVertInvert], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock x:Name="cursorSensitivityValue" DockPanel.Dock="Right" Classes="FieldValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [CursorSensitivity], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="CursorSensitivitySlider" Minimum="1" Maximum="5" TickFrequency="1"
HorizontalAlignment="Stretch"
IsSnapToTickEnabled="True" Value="3"
ValueChanged="QualitySlider_OnValueChanged" />
</StackPanel>
<!-- Communication -->
<TabItem Header="{Binding [Communication], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="GamepadIgnoreCb" Content="{Binding [GamepadSignalIgnore], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="FeedbackCb" MinWidth="350" MaxWidth="350" SelectedIndex="0">
<ComboBoxItem Tag="0" Content="{Binding [FeedbackOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [FeedbackCASHP], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="2" Content="{Binding [FeedbackCA], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="3" Content="{Binding [FeedbackLPT], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="4" Content="{Binding [FeedbackPoKeys], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel x:Name="FeedbackPortRow" Spacing="8" Orientation="Horizontal" IsVisible="False">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [FeedbackPort], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888" Width="120" />
</StackPanel>
<Expander x:Name="UartExpander" IsVisible="False" Header="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="UartEnableCb" Content="{Binding [UartEnable], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [UartPort], Source={x:Static starterNg:App.Loc}}" />
<TextBox x:Name="UartPortTb" MinWidth="150" PlaceholderText="COM3" />
<CheckBox x:Name="MouseHorInvertCb" Content="{Binding [MouseHorInvert], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MouseVertInvertCb" Content="{Binding [MouseVertInvert], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [UartTune], Source={x:Static starterNg:App.Loc}}" />
<TextBox x:Name="UartTuneTb" MinWidth="150" />
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [Communication], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="GamepadIgnoreCb" Content="{Binding [GamepadSignalIgnore], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="FeedbackCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Tag="0" Content="{Binding [FeedbackOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [FeedbackCASHP], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="2" Content="{Binding [FeedbackCA], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="3" Content="{Binding [FeedbackLPT], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="4" Content="{Binding [FeedbackPoKeys], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel x:Name="FeedbackPortRow" Classes="Field" IsVisible="False">
<TextBlock Classes="FieldLabel"
Text="{Binding [FeedbackPort], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888"
Increment="1" FormatString="0"
HorizontalAlignment="Left" Width="140" />
</StackPanel>
<Button x:Name="ComButton" Classes="Flat" Cursor="Hand"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
IsVisible="False" Click="ComButton_OnClick"
Content="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<CheckBox x:Name="UartDebugCb" Content="{Binding [UartDebug], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartMainCb" Content="{Binding [UartMain], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartScndCb" Content="{Binding [UartScnd], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartTrainCb" Content="{Binding [UartTrain], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartLocalCb" Content="{Binding [UartLocal], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartRadioVolumeCb" Content="{Binding [UartRadioVolume], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartRadioChannelCb" Content="{Binding [UartRadioChannel], Source={x:Static starterNg:App.Loc}}" />
</HeaderedContentControl>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="10">
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [Others], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="SelectExeCb" HorizontalAlignment="Stretch" SelectedIndex="0" />
</StackPanel>
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="BatteryDefaultCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [BatteryDefaultScenario], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOn], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel>
</HeaderedContentControl>
</StackPanel>
<HeaderedContentControl Grid.Column="4" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [SettingsProfile], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="6">
<ComboBox x:Name="ProfileCb" HorizontalAlignment="Stretch" />
<Button x:Name="ProfileApplyBtn" Classes="Flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding [ProfileApply], Source={x:Static starterNg:App.Loc}}"
Click="ProfileApplyButton_OnClick" />
<Button x:Name="ProfileSaveAsBtn" Classes="Flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding [ProfileSaveAs], Source={x:Static starterNg:App.Loc}}"
Click="ProfileSaveAsButton_OnClick" />
</StackPanel>
</Expander>
</StackPanel>
</HeaderedContentControl>
</Grid>
</ScrollViewer>
</TabItem>
<!-- Controls / key bindings -->
<TabItem Header="{Binding [Controls], Source={x:Static starterNg:App.Loc}}">
<Grid x:Name="ControlsHost" RowDefinitions="Auto,*,Auto" Margin="12">
<!-- Built in code-behind (BuildControlsTab): a search bar, the
scrollable binding list, and the keyboard visualisation. -->
</Grid>
</TabItem>
<!-- Others -->
<TabItem Header="{Binding [Others], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="SelectExeCb" MinWidth="350" MaxWidth="350" SelectedIndex="0" />
</StackPanel>
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Graphic -->
<TabItem Header="{Binding [Graphic], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="RenderEngineCb" MinWidth="150" MaxWidth="150" SelectedIndex="0">
<ComboBoxItem Content="{Binding [REFull], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOld], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimpShader], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimp], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REExperimental], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Grid Margin="10" ColumnDefinitions="250,10,250,10,250">
<HeaderedContentControl Grid.Column="0" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColResolution], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ResolutionCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem>3840x2160</ComboBoxItem>
<ComboBoxItem>2560x1440</ComboBoxItem>
<ComboBoxItem>1920x1080</ComboBoxItem>
<ComboBoxItem>1600x900</ComboBoxItem>
<ComboBoxItem>1366x768</ComboBoxItem>
<ComboBoxItem>1280x720</ComboBoxItem>
</ComboBox>
</StackPanel>
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #bufferScale.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [BufforResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="bufferScale" Minimum="50" Maximum="200" TickFrequency="1" IsSnapToTickEnabled="True" Value="100" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Name="texResolution" Text="4096 px" />
<TextBlock Classes="FieldLabel"
Text="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="textureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1" IsSnapToTickEnabled="True" Value="12" ValueChanged="TextureResolutionSlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Name="cabTexResolution" Text="4096 px" />
<TextBlock Classes="FieldLabel"
Text="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="cabTextureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1" IsSnapToTickEnabled="True" Value="12" ValueChanged="CabTextureResolutionSlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="texFilteringValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="TexFilteringSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="5" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="multisamplingValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="MultisamplingSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="3" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="trackCurvesValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [TrackCurvesQuality], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="TrackCurvesSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="renderRangeValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="RenderRangeSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="3" TickFrequency="1" IsSnapToTickEnabled="True" Value="1" HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Grid.Column="2" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColRendering], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="RenderEngineCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [REFull], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOld], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimpShader], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimp], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REExperimental], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AdditionalShadersCb" Content="{Binding [AdditionalShadersEffects], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ChromaticAberrationCb" Content="{Binding [ChromaticAberration], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MotionBlurCb" Content="{Binding [MotionBlur], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SkipPipelineCb" Content="SkipPipeline" />
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #FpsLimitSlider.Value, StringFormat='{}{0:0} FPS'}" />
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="FpsLimitSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="1" IsSnapToTickEnabled="True" Value="30"
IsEnabled="{Binding #FpsLimitEnableCb.IsChecked}" />
</StackPanel>
<Separator />
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field" IsEnabled="{Binding #SmokeDisplayCb.IsChecked}">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="smokeParticlesValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [SmokeParticlesMultiplier], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="SmokeParticlesSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="2" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #DynamicLightsSlider.Value, StringFormat='{}{0:0}'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="DynamicLightsSlider" Minimum="0" Maximum="7" TickFrequency="1" IsSnapToTickEnabled="True" Value="7" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #fovSlider.Value, StringFormat='{}{0:0}°'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="fovSlider" Minimum="5" Maximum="110" TickFrequency="5" IsSnapToTickEnabled="True" Value="30" HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<StackPanel Grid.Column="4" Spacing="10">
<HeaderedContentControl VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColShadows], Source={x:Static starterNg:App.Loc}}">
<HeaderedContentControl.Tag>
<CheckBox x:Name="RenderShadowsCb" FontSize="11" MinHeight="0"
Content="{Binding [RenderShadows], Source={x:Static starterNg:App.Loc}}" />
</HeaderedContentControl.Tag>
<StackPanel Spacing="8" IsEnabled="{Binding #RenderShadowsCb.IsChecked}">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ShadowDisplayCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [CSROnlyImportant], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRLimited], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRAll], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="shaderResolution" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ShadowsResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="shaderResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1" IsSnapToTickEnabled="True" Value="12" ValueChanged="shaderResolutionSlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="shaderRangeValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="shaderRange" Minimum="25" Maximum="400" TickFrequency="5" IsSnapToTickEnabled="True" Value="150"
ValueChanged="QualitySlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="cabShadowRangeValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [CabShadowsRange], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="cabShaderSourceRange" Minimum="0" Maximum="100" TickFrequency="10" IsSnapToTickEnabled="True" Value="30"
ValueChanged="QualitySlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #ShadowAngleLimitSlider.Value, StringFormat='{}{0:0.0}'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="ShadowAngleLimitSlider" Minimum="0.2" Maximum="1.0" TickFrequency="0.1" Value="0.2" HorizontalAlignment="Stretch" />
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldHint"
Text="{Binding [ShadowLimitShort], Source={x:Static starterNg:App.Loc}}" />
<TextBlock Classes="FieldHint"
Text="{Binding [ShadowLimitNone], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColReflections], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="ReflectionsCubeMapCb" Content="{Binding [ReflectionsCubeMap], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ReflectionsDetailsCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [RDTerrain], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModels], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModelsVehicles], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="reflectionsFramerateValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="reflectionsFramerate" Minimum="5" Maximum="120" TickFrequency="5" IsSnapToTickEnabled="True" Value="30"
ValueChanged="QualitySlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColScripts], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="RenderScreensCb" Content="{Binding [RenderScreens], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [PyScreenPriority], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="PyScreenPriorityCb" HorizontalAlignment="Stretch">
<ComboBoxItem Tag="normal" Content="{Binding [PyPriorityNormal], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="lower" Content="{Binding [PyPriorityLower], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="lowest" Content="{Binding [PyPriorityLowest], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="idle" Content="{Binding [PyPriorityIdle], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="off" Content="{Binding [PyPriorityOff], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel>
</HeaderedContentControl>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ResolutionCb" MinWidth="150" MaxWidth="150" SelectedIndex="0">
<ComboBoxItem>3840x2160</ComboBoxItem>
<ComboBoxItem>2560x1440</ComboBoxItem>
<ComboBoxItem>1920x1080</ComboBoxItem>
<ComboBoxItem>1600x900</ComboBoxItem>
<ComboBoxItem>1366x768</ComboBoxItem>
<ComboBoxItem>1280x720</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BufforResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="bufferScale" Minimum="50" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="200" TickFrequency="1" IsSnapToTickEnabled="True" Value="100" />
<TextBlock VerticalAlignment="Center" Text="{Binding #bufferScale.Value, StringFormat='{}{0}%'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="textureResolutionSlider" Minimum="9" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="14" ValueChanged="TextureResolutionSlider_OnValueChanged"
TickFrequency="1" IsSnapToTickEnabled="True" Value="12" />
<TextBlock Name="texResolution" VerticalAlignment="Center" Text="4096" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="cabTextureResolutionSlider" Minimum="9" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="14" ValueChanged="CabTextureResolutionSlider_OnValueChanged"
TickFrequency="1" IsSnapToTickEnabled="True" Value="12" />
<TextBlock Name="cabTexResolution" VerticalAlignment="Center" Text="4096" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TexFilteringSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="5" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="MultisamplingSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="3" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="DynamicLightsSlider" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="7" TickFrequency="1" IsSnapToTickEnabled="True" Value="7" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="RenderRangeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3" Value="1" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TrackCurvesQuality], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TrackCurvesSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="FpsLimitSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="1" IsSnapToTickEnabled="True" Value="30" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="ShadowAngleLimitSlider" Minimum="0.2" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="1.0" TickFrequency="0.1" Value="0.2" />
</StackPanel>
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [SmokeParticlesMultiplier], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="SmokeParticlesSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="2" />
</StackPanel>
<CheckBox x:Name="ChromaticAberrationCb" Content="{Binding [ChromaticAberration], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MotionBlurCb" Content="{Binding [MotionBlur], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AdditionalShadersCb" Content="{Binding [AdditionalShadersEffects], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ReflectionsCubeMapCb" Content="{Binding [ReflectionsCubeMap], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderShadowsCb" Content="{Binding [RenderShadows], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="reflectionsFramerate" Minimum="5" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="120" TickFrequency="5" IsSnapToTickEnabled="True" Value="30" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #reflectionsFramerate.Value, StringFormat='{}{0} FPS'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowsResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="shaderResolutionSlider" Minimum="9" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="14" ValueChanged="shaderResolutionSlider_OnValueChanged"
TickFrequency="1" IsSnapToTickEnabled="True" Value="12" />
<TextBlock Name="shaderResolution" VerticalAlignment="Center" Text="4096 px" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="shaderRange" Minimum="25" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="400" TickFrequency="5" IsSnapToTickEnabled="True" Value="150" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #shaderRange.Value, StringFormat='{}{0} m'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [CabShadowsRange], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="cabShaderSourceRange" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="10" IsSnapToTickEnabled="True" Value="30" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #cabShaderSourceRange.Value, StringFormat='{}{0} m'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ShadowDisplayCb" MinWidth="150" MaxWidth="150" SelectedIndex="0">
<ComboBoxItem Content="{Binding [CSROnlyImportant], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRLimited], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRAll], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ReflectionsDetailsCb" MinWidth="350" MaxWidth="350" SelectedIndex="0">
<ComboBoxItem Content="{Binding [RDTerrain], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModels], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModelsVehicles], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="fovSlider" Minimum="5" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="110" TickFrequency="5" IsSnapToTickEnabled="True" Value="30" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #fovSlider.Value, StringFormat='{}{0}°'}" />
</StackPanel>
<CheckBox x:Name="RenderScreensCb" Content="{Binding [RenderScreens], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</Grid>
</ScrollViewer>
</TabItem>
<!-- Physics -->
<TabItem Header="{Binding [Physics], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="PhysicsAccuracyCb" Content="{Binding [PhysicsAccuracyIncreased], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PantographBreakCb" Content="{Binding [BreakingPantographPossibility], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="OverheadOnlyCb" Content="{Binding [PowerOverheadLineOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SpeedometerTapesCb" Content="{Binding [SaveSpeedometerTapes], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SimLogsCb" Content="{Binding [SaveSimLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Friction], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="FrictionSlider" Minimum="0.1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="BrakeStepSlider" Minimum="0.4" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
</StackPanel>
<TabItem Header="{Binding [SoundPhysics], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Grid ColumnDefinitions="360,10,360" Margin="10">
<HeaderedContentControl Grid.Column="0" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="EnableSoundsCb" Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #VolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [Volume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="VolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #RadioVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="RadioVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #VehiclesVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="VehiclesVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #PositionalVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="PositionalVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #AmbientVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="AmbientVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #VolumePausedSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [VolumePaused], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="VolumePausedSlider" Minimum="0" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Grid.Column="2" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [Physics], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="PhysicsAccuracyCb" Content="{Binding [PhysicsAccuracyIncreased], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PantographBreakCb" Content="{Binding [BreakingPantographPossibility], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="OverheadOnlyCb" Content="{Binding [PowerOverheadLineOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SpeedometerTapesCb" Content="{Binding [SaveSpeedometerTapes], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SimLogsCb" Content="{Binding [SaveSimLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DebugLogVisibleCb" Content="{Binding [DebugLogVisible], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
<Separator />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #FrictionSlider.Value, StringFormat='{}{0:0.00}'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [Friction], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="FrictionSlider" Minimum="0.1" HorizontalAlignment="Stretch"
Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #BrakeStepSlider.Value, StringFormat='{}{0:0.00}'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="BrakeStepSlider" Minimum="0.4" HorizontalAlignment="Stretch"
Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #BrakeSpeedSlider.Value, StringFormat='{}{0:0.00}'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" HorizontalAlignment="Stretch"
Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
</Grid>
</ScrollViewer>
</TabItem>
<!-- Sounds -->
<TabItem Header="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="EnableSoundsCb" Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Volume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="VolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="RadioVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="VehiclesVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="PositionalVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="AmbientVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Controls], Source={x:Static starterNg:App.Loc}}">
<Grid x:Name="ControlsHost" RowDefinitions="Auto,*,Auto" Margin="10">
</Grid>
</TabItem>
<!-- Advanced -->
<TabItem Header="{Binding [Advanced], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="CompressTexturesCb" Content="{Binding [CompressTextures], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ScaleSpecularsCb" Content="{Binding [ScaleSpeculars], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UseGLESCb" Content="{Binding [UseGLES], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ShaderGammaCb" Content="{Binding [ShaderGamma], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ScreenMipmapsCb" Content="{Binding [ScreenMipmaps], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ExtendedModelConversionCb" Content="{Binding [ExtendedModelConversion], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="GfxResourceMoveCb" Content="{Binding [GfxResourceMove], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="GfxResourceSweepCb" Content="{Binding [GfxResourceSweep], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TrainPhysicsThreads], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TrainPhysicsThreadsSlider" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="0" TickFrequency="1" IsSnapToTickEnabled="True" Value="0" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #TrainPhysicsThreadsSlider.Value}" />
</StackPanel>
<CheckBox x:Name="IgnoreIrrelevantTrainsCb" Content="{Binding [IgnoreIrrelevantTrains], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Starter -->
<TabItem Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding [SettingsProfile], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ProfileCb" MinWidth="220" />
<Button x:Name="ProfileApplyBtn" Content="{Binding [ProfileApply], Source={x:Static starterNg:App.Loc}}"
Click="ProfileApplyButton_OnClick" />
<Button x:Name="ProfileSaveAsBtn" Content="{Binding [ProfileSaveAs], Source={x:Static starterNg:App.Loc}}"
Click="ProfileSaveAsButton_OnClick" />
</StackPanel>
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="HideArchivalVehiclesCb" Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="BatteryDefaultCb" MinWidth="220" SelectedIndex="0">
<ComboBoxItem Content="{Binding [BatteryDefaultScenario], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOn], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</UserControl>

View File

@@ -26,18 +26,22 @@ public partial class Settings : UserControl
{
InitializeComponent();
TrainPhysicsThreadsSlider.Maximum = Environment.ProcessorCount;
SettingsPathText.Text = StarterNG.Classes.Settings.Instance.LoadedFrom;
ToolTip.SetTip(SettingsPathText, StarterNG.Classes.Settings.Instance.LoadedFrom);
this.AttachedToVisualTree += (_, _) =>
{
TextureResolutionSlider_OnValueChanged(null, null);
CabTextureResolutionSlider_OnValueChanged(null, null);
shaderResolutionSlider_OnValueChanged(null, null);
UpdateQualityCaptions();
};
FillLanguageCombo();
App.Loc.LanguageChanged += SelectActiveLanguage;
App.Loc.LanguageChanged += UpdateQualityCaptions;
App.Loc.LanguageChanged += () => shaderResolutionSlider_OnValueChanged(null, null);
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb);
@@ -52,6 +56,15 @@ public partial class Settings : UserControl
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
}
private static int IndexOfTag(ComboBox cb, string? tag)
{
for (int i = 0; i < cb.Items.Count; i++)
if (cb.Items[i] is ComboBoxItem item &&
string.Equals(item.Tag as string, tag, StringComparison.OrdinalIgnoreCase))
return i;
return -1;
}
private static int FindComboIndexByContent(ComboBox cb, string content)
{
for (int i = 0; i < cb.Items.Count; i++)
@@ -146,16 +159,6 @@ public partial class Settings : UserControl
GamepadIgnoreCb.IsChecked = s.IgnoreGamepad;
FeedbackCb.SelectedIndex = s.FeedbackMode;
FeedbackPortNud.Value = (decimal)s.FeedbackPort;
UartEnableCb.IsChecked = s.UartEnabled;
UartPortTb.Text = s.UartPort;
UartTuneTb.Text = s.UartTune;
UartDebugCb.IsChecked = s.UartDebug;
UartMainCb.IsChecked = s.UartMain;
UartScndCb.IsChecked = s.UartScnd;
UartTrainCb.IsChecked = s.UartTrain;
UartLocalCb.IsChecked = s.UartLocal;
UartRadioVolumeCb.IsChecked = s.UartRadioVolume;
UartRadioChannelCb.IsChecked = s.UartRadioChannel;
UpdateFeedbackDetailVisibility();
if (s.SelectExeAutomatically)
@@ -229,28 +232,22 @@ public partial class Settings : UserControl
VehiclesVolumeSlider.Value = s.VehiclesVolume;
PositionalVolumeSlider.Value = s.PositionalVolume;
AmbientVolumeSlider.Value = s.AmbientVolume;
CompressTexturesCb.IsChecked = s.CompressTextures;
ScaleSpecularsCb.IsChecked = s.ScaleSpeculars;
UseGLESCb.IsChecked = s.UseGLES;
ShaderGammaCb.IsChecked = s.ShaderGamma;
ScreenMipmapsCb.IsChecked = s.ScreenMipmaps;
ExtendedModelConversionCb.IsChecked = s.ExtendedModelConversion;
GfxResourceMoveCb.IsChecked = s.GfxResourceMove;
GfxResourceSweepCb.IsChecked = s.GfxResourceSweep;
TrainPhysicsThreadsSlider.Value = s.TrainPhysicsThreads;
IgnoreIrrelevantTrainsCb.IsChecked = s.IgnoreIrrelevantTrains;
VolumePausedSlider.Value = s.VolumePaused;
SkipPipelineCb.IsChecked = s.SkipPipeline;
DebugLogVisibleCb.IsChecked = s.DebugLogVisible;
PyScreenPriorityCb.SelectedIndex = Math.Max(0, IndexOfTag(PyScreenPriorityCb, s.PyScreenPriority));
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree;
HideArchivalVehiclesCb.IsChecked = s.HideArchivalVehicles;
BatteryDefaultCb.SelectedIndex = (int)s.BatteryDefault;
}
finally
{
_loading = false;
}
UpdateQualityCaptions();
}
private void ReadFromUi()
@@ -269,16 +266,6 @@ public partial class Settings : UserControl
s.IgnoreGamepad = IsChecked(GamepadIgnoreCb);
s.FeedbackMode = Math.Max(0, FeedbackCb.SelectedIndex);
s.FeedbackPort = (int)(FeedbackPortNud.Value ?? 888);
s.UartEnabled = IsChecked(UartEnableCb);
s.UartPort = UartPortTb.Text ?? "";
s.UartTune = UartTuneTb.Text ?? "";
s.UartDebug = IsChecked(UartDebugCb);
s.UartMain = IsChecked(UartMainCb);
s.UartScnd = IsChecked(UartScndCb);
s.UartTrain = IsChecked(UartTrainCb);
s.UartLocal = IsChecked(UartLocalCb);
s.UartRadioVolume = IsChecked(UartRadioVolumeCb);
s.UartRadioChannel = IsChecked(UartRadioChannelCb);
s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0;
s.ExecutablePath = s.SelectExeAutomatically ? "eu07.exe"
@@ -340,25 +327,29 @@ public partial class Settings : UserControl
s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
s.PositionalVolume = (int)PositionalVolumeSlider.Value;
s.AmbientVolume = (int)AmbientVolumeSlider.Value;
s.CompressTextures = IsChecked(CompressTexturesCb);
s.ScaleSpeculars = IsChecked(ScaleSpecularsCb);
s.UseGLES = IsChecked(UseGLESCb);
s.ShaderGamma = IsChecked(ShaderGammaCb);
s.ScreenMipmaps = IsChecked(ScreenMipmapsCb);
s.ExtendedModelConversion = IsChecked(ExtendedModelConversionCb);
s.GfxResourceMove = IsChecked(GfxResourceMoveCb);
s.GfxResourceSweep = IsChecked(GfxResourceSweepCb);
s.TrainPhysicsThreads = (int)TrainPhysicsThreadsSlider.Value;
s.IgnoreIrrelevantTrains = IsChecked(IgnoreIrrelevantTrainsCb);
s.VolumePaused = (int)VolumePausedSlider.Value;
s.SkipPipeline = IsChecked(SkipPipelineCb);
s.DebugLogVisible = IsChecked(DebugLogVisibleCb);
s.PyScreenPriority = (PyScreenPriorityCb.SelectedItem as ComboBoxItem)?.Tag as string ?? "normal";
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb);
s.HideArchivalVehicles = IsChecked(HideArchivalVehiclesCb);
s.BatteryDefault = (BatteryDefault)Math.Max(0, BatteryDefaultCb.SelectedIndex);
}
private void ComButton_OnClick(object? sender, RoutedEventArgs e)
{
if (TopLevel.GetTopLevel(this) is Window owner)
new UartWindow().ShowDialog(owner);
}
private void AboutButton_OnClick(object? sender, RoutedEventArgs e)
{
if (TopLevel.GetTopLevel(this) is Window owner)
AboutWindow.Create().ShowDialog(owner);
}
private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
{
ReadFromUi();
@@ -436,6 +427,7 @@ public partial class Settings : UserControl
int resolution = 1 << (int)shaderResolutionSlider.Value;
shaderResolution.Text = $"{resolution} px";
ToolTip.SetTip(shaderResolutionSlider, ShadowMapWord(resolution));
}
private void FillLanguageCombo()
@@ -475,7 +467,7 @@ public partial class Settings : UserControl
private void UpdateFeedbackDetailVisibility()
{
FeedbackPortRow.IsVisible = FeedbackCb.SelectedIndex == 3;
UartExpander.IsVisible = FeedbackCb.SelectedIndex == 5;
ComButton.IsVisible = FeedbackCb.SelectedIndex == 5;
}
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
@@ -535,7 +527,7 @@ public partial class Settings : UserControl
VerticalAlignment = VerticalAlignment.Center,
Foreground = FgDimBrush
});
_controlsSearch = new TextBox { Width = 240, Watermark = App.Loc["Search"] };
_controlsSearch = new TextBox { Width = 240, PlaceholderText = App.Loc["Search"] };
_controlsSearch.TextChanged += (_, _) => RebuildBindingList();
topBar.Children.Add(_controlsSearch);
@@ -1068,4 +1060,103 @@ public partial class Settings : UserControl
private static string Capitalize(string s) =>
string.IsNullOrEmpty(s) ? s : char.ToUpperInvariant(s[0]) + s.Substring(1);
private static string[] QualityWords => new[]
{
App.Loc["QualityVeryLow"], App.Loc["QualityLow"], App.Loc["QualityNormal"],
App.Loc["QualityHigh"], App.Loc["QualityVeryHigh"]
};
private void QualitySlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
=> UpdateQualityCaptions();
private void UpdateQualityCaptions()
{
var q = QualityWords;
if (texFilteringValue != null)
texFilteringValue.Text = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1];
string[] four = { q[0], q[1], q[3], q[4] };
if (trackCurvesValue != null)
trackCurvesValue.Text = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1];
if (smokeParticlesValue != null)
smokeParticlesValue.Text = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1];
string[] msaa = { App.Loc["MsaaNone"], "x2", "x4", "x8" };
if (multisamplingValue != null)
multisamplingValue.Text = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1];
string[] range = { App.Loc["RangeNormal"], App.Loc["RangeHigh"], App.Loc["RangeVeryHigh"] };
if (renderRangeValue != null)
renderRangeValue.Text = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1];
if (cursorSensitivityValue != null)
cursorSensitivityValue.Text = (int)CursorSensitivitySlider.Value switch
{
<= 1 => q[1],
2 => App.Loc["QualityStandard"],
3 => q[3],
_ => q[4]
};
if (shaderRangeValue != null)
{
int metres = (int)shaderRange.Value;
shaderRangeValue.Text = $"{metres} m";
ToolTip.SetTip(shaderRange, ShadowRangeWord(metres));
}
if (cabShadowRangeValue != null)
{
int metres = (int)cabShaderSourceRange.Value;
cabShadowRangeValue.Text = metres <= 0 ? App.Loc["RangeDisabled"] : $"{metres} m";
ToolTip.SetTip(cabShaderSourceRange, CabShadowRangeWord(metres));
}
if (reflectionsFramerateValue != null)
{
int fps = (int)reflectionsFramerate.Value;
string word = ReflectionsRefreshWord(fps);
reflectionsFramerateValue.Text = $"{fps} FPS";
ToolTip.SetTip(reflectionsFramerate, word.Length == 0 ? null : word);
}
}
private static string ShadowMapWord(int pixels) => pixels switch
{
<= 512 => App.Loc["QualityVeryLow"],
<= 1024 => App.Loc["QualityLow"],
<= 2048 => App.Loc["QualityModerate"],
<= 4096 => App.Loc["QualityHigh"],
_ => App.Loc["QualityVeryHigh"]
};
private static string ShadowRangeWord(int metres) => metres switch
{
<= 25 => App.Loc["RangeVeryLow"],
<= 50 => App.Loc["RangeLow"],
<= 150 => App.Loc["RangeModerate"],
<= 250 => App.Loc["RangeHigh"],
_ => App.Loc["RangeVeryHigh"]
};
private static string CabShadowRangeWord(int metres) => metres switch
{
<= 10 => App.Loc["RangeVeryLow"],
<= 20 => App.Loc["RangeLow"],
<= 30 => App.Loc["RangeStandard"],
<= 50 => App.Loc["RangeHigh"],
_ => App.Loc["RangeVeryHigh"]
};
private static string ReflectionsRefreshWord(int fps) => fps switch
{
<= 5 => App.Loc["QualityVeryLow"],
<= 10 => App.Loc["QualityLow"],
<= 25 => App.Loc["QualityHigh"],
<= 60 => App.Loc["QualityVeryHigh"],
_ => string.Empty
};
}

View File

@@ -8,10 +8,10 @@
WindowStartupLocation="CenterOwner"
MinWidth="720" MinHeight="400">
<DockPanel Margin="10">
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*,*,*" RowDefinitions="Auto,Auto,Auto"
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*,*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto"
ColumnSpacing="8" RowSpacing="6" Margin="0,0,0,8">
<CheckBox Name="fNameOn" Grid.Row="0" Grid.Column="0" Content="{Binding [TextureInfo], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fName" Grid.Row="0" Grid.Column="1" Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fName" Grid.Row="0" Grid.Column="1" PlaceholderText="{Binding [Search], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="fOpOn" Grid.Row="0" Grid.Column="2" Content="{Binding [TexOperator], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fOp" Grid.Row="0" Grid.Column="3" />
@@ -24,15 +24,44 @@
<TextBox Name="fStation" Grid.Row="2" Grid.Column="1" />
<CheckBox Name="fPhotoOn" Grid.Row="2" Grid.Column="2" Content="{Binding [TexPhoto], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fPhoto" Grid.Row="2" Grid.Column="3" />
<CheckBox Name="fModelOn" Grid.Row="3" Grid.Column="0" Content="{Binding [Model], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fModel" Grid.Row="3" Grid.Column="1" />
<CheckBox Name="fRevFromOn" Grid.Row="4" Grid.Column="0" Content="{Binding [RevisionFrom], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fRevFrom" Grid.Row="4" Grid.Column="1" PlaceholderText="RRRR-MM-DD" />
<CheckBox Name="fRevToOn" Grid.Row="4" Grid.Column="2" Content="{Binding [RevisionTo], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fRevTo" Grid.Row="4" Grid.Column="3" PlaceholderText="RRRR-MM-DD" />
</Grid>
<TextBlock Name="statusText" DockPanel.Dock="Bottom" Margin="0,6,0,0"
FontSize="11" Opacity="0.75" />
<Grid DockPanel.Dock="Left" Width="260" RowDefinitions="*,4,*">
<ListBox Name="typeList" Grid.Row="0" Classes="Compact" />
<GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch"
Background="Transparent" ResizeDirection="Rows" />
<ListBox Name="modelList" Grid.Row="2" Classes="Compact">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="28,90,*" ColumnSpacing="4">
<TextBlock Grid.Column="0" Text="{Binding No}" Opacity="0.6" />
<TextBlock Grid.Column="1" Text="{Binding Klasa}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Text="{Binding Sciezka}" Opacity="0.75"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<GridSplitter Name="spSplitter" DockPanel.Dock="Left" Width="4"
Background="Transparent" ResizeDirection="Columns" />
<ListBox Name="textureList" DoubleTapped="TextureList_OnDoubleTapped">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="220,*,160,100,140" ColumnSpacing="8">
<Grid ColumnDefinitions="1.4*,0.9*,1.2*,90,*" ColumnSpacing="8">
<TextBlock Grid.Column="0" Text="{Binding Skin}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" Text="{Binding Mini}" Opacity="0.85" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Text="{Binding Operator}" Opacity="0.85" TextTrimming="CharacterEllipsis" />

View File

@@ -28,21 +28,65 @@ public partial class TextureBaseWindow : Window
_all.Add(new Row(t, _db));
}
foreach (var box in new Control[] { fName, fOp, fMini, fAuthor, fStation, fPhoto })
foreach (var box in new Control[] { fName, fOp, fMini, fAuthor, fStation, fPhoto, fModel })
{
if (box is TextBox tb)
tb.TextChanged += (_, _) => Refresh();
}
foreach (var box in new Control[] { fNameOn, fOpOn, fMiniOn, fAuthorOn, fStationOn, fPhotoOn })
foreach (var box in new Control[] { fNameOn, fOpOn, fMiniOn, fAuthorOn, fStationOn, fPhotoOn,
fModelOn, fRevFromOn, fRevToOn })
{
if (box is CheckBox cb)
cb.IsCheckedChanged += (_, _) => Refresh();
}
fRevFrom.TextChanged += (_, _) => Refresh();
fRevTo.TextChanged += (_, _) => Refresh();
BuildTypeList();
typeList.SelectionChanged += (_, _) => { BuildModelList(); Refresh(); };
modelList.SelectionChanged += (_, _) => Refresh();
textureList.ContextRequested += TextureList_OnContextRequested;
Refresh();
}
private sealed record ModelRow(string No, string Klasa, string Sciezka);
private void BuildModelList()
{
modelList.ItemsSource = null;
string? cat = (typeList.SelectedItem as ListBoxItem)?.Tag as string;
var rows = _all
.Where(r => cat == null || string.Equals(r.Category, cat, StringComparison.OrdinalIgnoreCase))
.Where(r => r.Mini.Length > 0)
.GroupBy(r => r.Mini, StringComparer.OrdinalIgnoreCase)
.OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase)
.Select((g, i) => new ModelRow($"{i + 1}.", g.Key, g.First().Directory))
.ToList();
modelList.ItemsSource = rows;
}
private static DateTime? ParseDate(string? text) =>
DateTime.TryParse(text, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var d) ? d : null;
private void BuildTypeList()
{
typeList.Items.Add(new ListBoxItem { Content = App.Loc["CatAll"], Tag = null });
foreach (string cat in _all.Select(r => r.Category)
.Where(c => c.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c, StringComparer.OrdinalIgnoreCase))
typeList.Items.Add(new ListBoxItem { Content = cat, Tag = cat });
typeList.SelectedIndex = 0;
BuildModelList();
}
private void Refresh()
{
IEnumerable<Row> q = _all;
@@ -53,6 +97,18 @@ public partial class TextureBaseWindow : Window
q = Filter(q, fAuthorOn, fAuthor, r => r.Author);
q = Filter(q, fStationOn, fStation, r => r.Station);
q = Filter(q, fPhotoOn, fPhoto, r => r.Photo);
q = Filter(q, fModelOn, fModel, r => r.Model);
if ((typeList.SelectedItem as ListBoxItem)?.Tag is string cat)
q = q.Where(r => string.Equals(r.Category, cat, StringComparison.OrdinalIgnoreCase));
if (modelList.SelectedItem is ModelRow mr)
q = q.Where(r => string.Equals(r.Mini, mr.Klasa, StringComparison.OrdinalIgnoreCase));
if (fRevFromOn.IsChecked == true && ParseDate(fRevFrom.Text) is { } from)
q = q.Where(r => r.RevisionDate is { } d && d >= from);
if (fRevToOn.IsChecked == true && ParseDate(fRevTo.Text) is { } to)
q = q.Where(r => r.RevisionDate is { } d && d <= to);
var list = q
.OrderBy(r => r.Mini, StringComparer.OrdinalIgnoreCase)
@@ -98,12 +154,23 @@ public partial class TextureBaseWindow : Window
{
string dir = Path.Combine(Directory.GetCurrentDirectory(), "dynamic",
row.Texture.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/'));
if (!Directory.Exists(dir)) return;
if (!Directory.Exists(dir))
dir = Path.Combine(Directory.GetCurrentDirectory(), row.Texture.Directory);
if (!Directory.Exists(dir))
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
string.Format(App.Loc["FaultFileNotFound"], dir));
return;
}
try
{
Process.Start(new ProcessStartInfo(dir) { UseShellExecute = true });
}
catch { }
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
$"{dir}{Environment.NewLine}{Environment.NewLine}{App.Loc["FaultDetail"]} {ex.Message}");
}
};
menu.Items.Add(open);
menu.Open(textureList);
@@ -120,6 +187,10 @@ public partial class TextureBaseWindow : Window
public string Author { get; }
public string Station { get; }
public string Photo { get; }
public string Model { get; }
public string Category { get; }
public string Directory { get; }
public DateTime? RevisionDate { get; }
public Row(VehicleTexture t, VehicleDatabase db)
{
@@ -131,6 +202,11 @@ public partial class TextureBaseWindow : Window
Author = t.Meta?.TextureAuthor ?? "";
Station = t.Meta?.Depot ?? "";
Photo = t.Meta?.PhotoAuthor ?? "";
Model = t.Model ?? "";
Category = t.ResolvedCategory ?? "";
Directory = t.Directory ?? "";
RevisionDate = DateTime.TryParse(Revision, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var rev) ? rev : null;
}
}
}

View File

@@ -0,0 +1,129 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using SettingsModel = StarterNG.Classes.Settings;
namespace StarterNG.Views;
public sealed class UartWindow : Window
{
private readonly CheckBox _enabled = new();
private readonly TextBox _port = new() { PlaceholderText = "COM3", MinWidth = 170 };
private readonly TextBox _tune = new() { MinWidth = 170 };
private readonly CheckBox _debug = new();
private readonly CheckBox _main = new();
private readonly CheckBox _scnd = new();
private readonly CheckBox _train = new();
private readonly CheckBox _local = new();
private readonly CheckBox _radioVolume = new();
private readonly CheckBox _radioChannel = new();
public UartWindow()
{
Title = App.Loc["UartSection"];
Width = 430;
SizeToContent = SizeToContent.Height;
CanResize = false;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
_enabled.Content = App.Loc["UartEnable"];
_debug.Content = App.Loc["UartDebug"];
_main.Content = App.Loc["UartMain"];
_scnd.Content = App.Loc["UartScnd"];
_train.Content = App.Loc["UartTrain"];
_local.Content = App.Loc["UartLocal"];
_radioVolume.Content = App.Loc["UartRadioVolume"];
_radioChannel.Content = App.Loc["UartRadioChannel"];
Load();
var root = new StackPanel { Margin = new Avalonia.Thickness(16), Spacing = 8 };
root.Children.Add(_enabled);
root.Children.Add(Row(App.Loc["UartPort"], _port));
root.Children.Add(Row(App.Loc["UartTune"], _tune));
root.Children.Add(new TextBlock
{
Text = App.Loc["UartHint"],
FontSize = 11,
Opacity = 0.65,
TextWrapping = TextWrapping.Wrap
});
root.Children.Add(_debug);
root.Children.Add(new TextBlock
{
Text = App.Loc["UartFeature"],
FontWeight = FontWeight.Bold,
Margin = new Avalonia.Thickness(0, 6, 0, 0)
});
var top = new UniformGrid { Columns = 2, Margin = new Avalonia.Thickness(0, 0, 0, 2) };
top.Children.Add(_main);
top.Children.Add(_scnd);
top.Children.Add(_train);
top.Children.Add(_local);
root.Children.Add(top);
var mid = new UniformGrid { Columns = 2 };
mid.Children.Add(_radioVolume);
mid.Children.Add(_radioChannel);
root.Children.Add(mid);
var save = new Button
{
Content = App.Loc["UartSaveClose"],
MinWidth = 140,
Margin = new Avalonia.Thickness(0, 10, 0, 0),
HorizontalAlignment = HorizontalAlignment.Right,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
save.Classes.Add("Accent");
save.Click += (_, _) => { Store(); Close(); };
root.Children.Add(save);
Content = root;
}
private static Control Row(string label, Control input)
{
var sp = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
sp.Children.Add(new TextBlock
{
Text = label,
VerticalAlignment = VerticalAlignment.Center,
MinWidth = 110
});
sp.Children.Add(input);
return sp;
}
private void Load()
{
var s = SettingsModel.Instance;
_enabled.IsChecked = s.UartEnabled;
_port.Text = s.UartPort;
_tune.Text = s.UartTune;
_debug.IsChecked = s.UartDebug;
_main.IsChecked = s.UartMain;
_scnd.IsChecked = s.UartScnd;
_train.IsChecked = s.UartTrain;
_local.IsChecked = s.UartLocal;
_radioVolume.IsChecked = s.UartRadioVolume;
_radioChannel.IsChecked = s.UartRadioChannel;
}
private void Store()
{
var s = SettingsModel.Instance;
s.UartEnabled = _enabled.IsChecked == true;
s.UartPort = _port.Text ?? "";
s.UartTune = _tune.Text ?? "";
s.UartDebug = _debug.IsChecked == true;
s.UartMain = _main.IsChecked == true;
s.UartScnd = _scnd.IsChecked == true;
s.UartTrain = _train.IsChecked == true;
s.UartLocal = _local.IsChecked == true;
s.UartRadioVolume = _radioVolume.IsChecked == true;
s.UartRadioChannel = _radioChannel.IsChecked == true;
}
}

View File

@@ -33,6 +33,8 @@
<String key="Time">Čas</String>
<String key="CurrentTime">Nyní</String>
<String key="Day">Den v roce</String>
<String key="Date">Datum</String>
<String key="DayOfYear">den {0}</String>
<String key="Sky">Obloha</String>
<String key="Fog">Mlha / viditelnost</String>
<String key="Season">Roční období</String>
@@ -44,6 +46,7 @@
<String key="SeasonSummer">Léto</String>
<String key="SeasonAutumn">Podzim</String>
<String key="SeasonWinter">Zima</String>
<String key="OvercastRandom">Náhodné</String>
<String key="OvercastClear">Jasno</String>
<String key="OvercastFair">Skoro jasno</String>
<String key="OvercastLight">Slabá oblačnost</String>
@@ -51,7 +54,8 @@
<String key="OvercastHeavy">Silná oblačnost</String>
<String key="OvercastCloudy">Zataženo</String>
<String key="OvercastStorm">Bouřka</String>
<String key="WeatherHint">Změny počasí se při spuštění zapíší do scenérie.</String>
<String key="WeatherDateTime">Datum a čas</String>
<String key="WeatherConditions">Povětrnostní podmínky</String>
<String key="Consist">Souprava</String>
<String key="Start">Start</String>
<String key="StartNoScenery">Vyberte scenérii</String>
@@ -148,40 +152,39 @@
<String key="Coupling">Spřáhlo</String>
<String key="MapConsists">Soupravy ve scenérii</String>
<String key="NoSceneryConsists">Vyberte scenérii na kartě Scénáře.</String>
<String key="BrakeMode">Režim brzdy</String>
<String key="BrakeLoad">Nastavení zatížení</String>
<String key="BrakeSwitch">Přepínač brzdy</String>
<String key="BrakeMode">Nastavení brzdiče</String>
<String key="BrakeLoad">Přispůsobení</String>
<String key="BrakeSwitch">Brzdič</String>
<String key="None">—</String>
<!-- Coupling mask bits -->
<String key="CplMechanical">Mechanické</String>
<String key="CplBrake">Pneumatické 5 atm (brzdy)</String>
<String key="CplControl">Vícečlenné řízení</String>
<String key="CplMechanical">Mechanicky</String>
<String key="CplBrake">Brzda</String>
<String key="CplControl">Ukrojení</String>
<String key="CplHighVoltage">Vysoké napětí</String>
<String key="CplGangway">Průchod mezi vozidly</String>
<String key="CplAuxPneumatic">Pomocné pneumatické 8 atm</String>
<String key="CplGangway">Přechod</String>
<String key="CplAuxPneumatic">Vzduch 8atm</String>
<String key="CplHeating">Topení</String>
<String key="CplWorkshop">Dílenské spřáhlo (zámek rozpojení)</String>
<String key="CplWorkshop">Blokáda</String>
<!-- Brake modes -->
<String key="BrakeFreight">Nákladní (G)</String>
<String key="BrakePassenger">Osobní (P)</String>
<String key="BrakeExpress">Rychlík (R)</String>
<String key="BrakeExpressMg">Rychlík + magnetická kolejnicová (R+Mg)</String>
<String key="BrakeNoAir">Start bez vzduchu (Q)</String>
<String key="BrakeOff">Brzdy odpojeny (O)</String>
<String key="BrakeAuto">Automaticky (A)</String>
<String key="BrakeActingNone">Není nastaven</String>
<String key="BrakeFreight">G - nákladní režim</String>
<String key="BrakePassenger">P - osobní režim</String>
<String key="BrakeExpress">R - rychlíkový režim</String>
<String key="BrakeExpressMg">R + magn. kolejová brzda</String>
<String key="BrakeNoAir">Q - bez vzduchu</String>
<!-- Load adaptation -->
<String key="LoadEmpty">Prázdný (T)</String>
<String key="LoadMedium">Ložený I — střední (H)</String>
<String key="LoadFull">Ložený II (F)</String>
<String key="LoadAuto">Automaticky (A)</String>
<String key="LoadNeutral">N - neutralní nastavení</String>
<String key="LoadEmpty">T - prázdný</String>
<String key="LoadMedium">H - naložen I</String>
<String key="LoadFull">F - naložen II</String>
<!-- Brake switch -->
<String key="SwitchOff">Vypnuto (0)</String>
<String key="SwitchOff10">Vypnuto s pravděpodobností 10 % (1)</String>
<String key="SwitchOn">Zapnuto (A)</String>
<String key="SwitchNormal">N - brzdič zabržděný</String>
<String key="SwitchOff">0 - odbržděný, odpuštěný</String>
<String key="SwitchOff10">1 - odbržděný, odp. z 10% pravd.</String>
<!-- Damage (wheel parameters) -->
<String key="Damage">Poškození</String>
@@ -192,6 +195,9 @@
<String key="LoadType">Druh nákladu</String>
<String key="LoadCount">Množství</String>
<String key="LoadHint">Pro naložení vozidla zadejte množství větší než 0.</String>
<String key="LoadNotLoadable">Toto vozidlo nepřijímá žádný náklad.</String>
<String key="LoadPantState">Zvednutý pantograf na startu</String>
<String key="LoadPantStateHint">Množství určuje pantograf: 1 — A, 2 — B, 3 — oba.</String>
<String key="WagonNumber">Číslo vozu</String>
<String key="LoadCopyPrev">Kopírovat náklad z předchozího vozidla</String>
<String key="LoadMax">Maximální možný náklad</String>
@@ -242,6 +248,10 @@
<String key="ComingSoon">Možnosti úprav budou brzy k dispozici.</String>
<String key="Split">Rozdělit</String>
<String key="TipSplit">Rozdělit jednotku na jednotlivé vozy</String>
<String key="AppliesToUnit">Společné pro celou jednotku</String>
<String key="CouplingUnitRear">Spřáhlo na konci jednotky</String>
<String key="Join">Spojit</String>
<String key="TipJoin">Spojit toto vozidlo s následujícím do jedné jednotky</String>
<String key="AddVehicle">Přidat</String>
<String key="TipAddVehicle">Přidat toto vozidlo do soupravy</String>
<String key="TipAddUnit">Přidat celou jednotku do soupravy</String>
@@ -320,10 +330,28 @@
<String key="MaxTexCabResolution">Maximální rozlišení textur kabiny:</String>
<String key="TexFilteringQuality">Kvalita filtrování textur:</String>
<String key="Multisampling">Multisampling:</String>
<String key="QualityVeryLow">Velmi nízká</String>
<String key="QualityLow">Nízká</String>
<String key="QualityNormal">Normální</String>
<String key="QualityHigh">Vysoká</String>
<String key="QualityVeryHigh">Velmi vysoká</String>
<String key="MsaaNone">Žádné</String>
<String key="QualityModerate">Mírná</String>
<String key="QualityStandard">Standartní</String>
<String key="RangeVeryLow">Velmi nízký</String>
<String key="RangeLow">Nízký</String>
<String key="RangeNormal">Normální</String>
<String key="RangeModerate">Mírný</String>
<String key="RangeStandard">Standartní</String>
<String key="RangeHigh">Vysoká</String>
<String key="RangeVeryHigh">Velmi vysoká</String>
<String key="RangeDisabled">Vypnuté</String>
<String key="DynamicLights">Dynamické zdroje světla:</String>
<String key="RenderRange">Dohlednost vykreslování:</String>
<String key="FpsLimitEnable">Omezit snímkovou frekvenci</String>
<String key="ShadowAngleLimit">Omezení délky stínů</String>
<String key="ShadowLimitNone">Bez omezení</String>
<String key="ShadowLimitShort">Zkrácené</String>
<String key="FullscreenWindowed">Automatické rozlišení (celá obrazovka bez okrajů)</String>
<String key="RenderAngleVulkan">Použít renderer ANGLE Vulkan</String>
<String key="VSync">VSync</String>
@@ -427,4 +455,100 @@
<String key="Save">Uložit</String>
<String key="Reset">Obnovit</String>
<String key="SettingsSaved">Nastavení uloženo</String>
<!-- Errors / diagnostics -->
<String key="FaultTitle">Chyba</String>
<String key="FaultDetail">Podrobnosti chyby:</String>
<String key="FaultFileNotFound">Soubor nenalezen: {0}</String>
<String key="FaultLoadData">Nepodařilo se načíst data simulátoru.</String>
<String key="FaultBadInstall">Instalace simulátoru může být neúplná.</String>
<String key="FaultNoDir">Chybí složka {0}</String>
<String key="FaultNoWeights">Chybí informace o hmotnostech nákladu.</String>
<String key="FaultNoScenery">Nebyly nalezeny žádné scénáře.</String>
<String key="FaultNoVehicles">Nebyla nalezena žádná vozidla.</String>
<String key="FaultNoPhysics">Nebyla nalezena data vozového parku.</String>
<String key="FaultNoExe">Spustitelný soubor simulátoru nebyl nalezen.</String>
<!-- About -->
<String key="AboutTitle">O programu</String>
<String key="AboutForSimulator">pro simulátor MaSzyna</String>
<String key="AboutChangelog">Seznam změn:</String>
<String key="AboutNoChangelog">Soubor se seznamem změn nebyl nalezen.</String>
<String key="About">O programu</String>
<String key="Close">Zavřít</String>
<String key="SettingsPath">Nastavení načteno z:</String>
<!-- Vehicle info -->
<String key="GeneralInfo">Obecné</String>
<String key="Model">Model</String>
<String key="VMax">VMax</String>
<String key="Texture">Textura</String>
<!-- Scenery tab -->
<String key="SceneryConsists">Soupravy na scenérii</String>
<String key="SceneryFaults">Chyby ve scénáři:</String>
<!-- Warehouse / vehicle panels -->
<String key="WarehouseConsists">Soupravy ve skladu</String>
<String key="AddToWarehouse">Přidat do skladu</String>
<String key="RemoveFromWarehouse">Odebrat ze skladu</String>
<String key="WarehouseApply">Nahradit soupravou ze skladu</String>
<String key="WarehouseImportNothing">Nebyly nalezeny žádné soupravy k importu.</String>
<String key="AboutTexture">O textuře</String>
<String key="NoTextureInfo">Žádné informace o textuře.</String>
<!-- Vehicle configuration / coupler -->
<String key="Configuration">Konfigurace</String>
<String key="RemoveVehicle">Odebrat vozidlo</String>
<String key="AutoCoupler">Auto</String>
<String key="TipAutoCoupler">Zvolit spřáhla automaticky pro celou soupravu</String>
<String key="CouplingLastVehicle">Spřáhlo na konci soupravy — za ním není vozidlo.</String>
<String key="ChangeName">Přejmenovat...</String>
<String key="LoadCopyToAll">Použít tento náklad na všechna následující vozidla, která ho přijmou</String>
<String key="WarehouseAppend">Připojit soupravu ze skladu</String>
<String key="WarehouseImportFile">Přidat soupravy ze souboru...</String>
<String key="StartWithoutSaving">Spustit bez uložení nastavení</String>
<String key="StartFreeFly">Spustit bez vozidla</String>
<String key="VolumePaused">Hlasitost při pauze:</String>
<String key="DebugLogVisible">Zobrazovat průběh simulace</String>
<String key="PyScreenPriority">Frekvence obnovení doplňkových obrazovek:</String>
<String key="PyPriorityNormal">Standardní</String>
<String key="PyPriorityLower">Nízká</String>
<String key="PyPriorityLowest">Velmi nízká</String>
<String key="PyPriorityIdle">Žádná</String>
<String key="PyPriorityOff">Vypnutá</String>
<String key="MassBrutto">Hrubá hmotnost</String>
<String key="VisibilityGood">Dobrá</String>
<String key="VisibilityBad">Špatná</String>
<String key="SoundPhysics">Zvuk | Fyzika</String>
<String key="DriverHead">Strojvedoucí (kabina A)</String>
<String key="DriverRear">Strojvedoucí (kabina B)</String>
<String key="DriverPassenger">Cestující</String>
<String key="DriverNobody">Nikdo</String>
<String key="RemoveAllTrains">Odebrat všechny soupravy ze scenérie</String>
<String key="RemoveAllTrainsConfirm">Odebrat vozidla ze všech souprav na této scenérii?</String>
<String key="TexWorks">Revize:</String>
<String key="RevisionFrom">Revize od:</String>
<String key="RevisionTo">Revize do:</String>
<String key="CrewLabel">Obsazení:</String>
<String key="RuleAdd">Přidat do pravidla</String>
<String key="RuleNew">— nové pravidlo —</String>
<String key="RuleRemove">Odebrat z pravidla</String>
<String key="CatAll">Vše</String>
<String key="GfxColResolution">Rozlišení a textury</String>
<String key="GfxColRendering">Vykreslování</String>
<String key="GfxColShadows">Stíny</String>
<String key="GfxColReflections">Odrazy a pohled</String>
<String key="GfxColScripts">Python skripty</String>
<String key="UartHint">Název portu / neměnit / frekvence odesílání dat</String>
<String key="UartFeature">UARTfeature:</String>
<String key="UartSaveClose">Uložit a zavřít</String>
<String key="PickCategoryFirst">Nejprve vyberte kategorii nebo třídu vozidel.</String>
<String key="HideArchivalSceneries">Skrýt archivní scénáře</String>
<String key="Tools">Nástroje</String>
<String key="AdvancedSettings">Pokročilá nastavení</String>
<String key="AdvLogPage">Deník událostí</String>
<String key="AdvClearLog">Vymazat log</String>
<String key="ConsistSchedule">Sestava</String>
</Language>

View File

@@ -33,6 +33,8 @@
<String key="Time">Time</String>
<String key="CurrentTime">Now</String>
<String key="Day">Day of year</String>
<String key="Date">Date</String>
<String key="DayOfYear">day {0}</String>
<String key="Sky">Sky</String>
<String key="Fog">Fog / visibility</String>
<String key="Season">Season</String>
@@ -44,6 +46,7 @@
<String key="SeasonSummer">Summer</String>
<String key="SeasonAutumn">Autumn</String>
<String key="SeasonWinter">Winter</String>
<String key="OvercastRandom">Random</String>
<String key="OvercastClear">Clear</String>
<String key="OvercastFair">Fair</String>
<String key="OvercastLight">Light clouds</String>
@@ -51,7 +54,8 @@
<String key="OvercastHeavy">Heavy clouds</String>
<String key="OvercastCloudy">Cloudy</String>
<String key="OvercastStorm">Storm</String>
<String key="WeatherHint">Weather changes are written to the scenery on launch.</String>
<String key="WeatherDateTime">Date and time</String>
<String key="WeatherConditions">Conditions</String>
<String key="Consist">Consist</String>
<String key="Start">Start</String>
<String key="StartNoScenery">Select a scenery</String>
@@ -148,40 +152,39 @@
<String key="Coupling">Coupling</String>
<String key="MapConsists">Consists on the scenery</String>
<String key="NoSceneryConsists">Select a scenery in the Scenarios tab.</String>
<String key="BrakeMode">Brake mode</String>
<String key="BrakeLoad">Load setting</String>
<String key="BrakeMode">Brake setting</String>
<String key="BrakeLoad">Brake adjustment</String>
<String key="BrakeSwitch">Brake switch</String>
<String key="None">—</String>
<!-- Coupling mask bits -->
<String key="CplMechanical">Mechanical</String>
<String key="CplBrake">Pneumatic 5 atm (brakes)</String>
<String key="CplControl">Multiple-unit control</String>
<String key="CplBrake">Brake</String>
<String key="CplControl">Connector</String>
<String key="CplHighVoltage">High voltage</String>
<String key="CplGangway">Gangway between vehicles</String>
<String key="CplAuxPneumatic">Auxiliary pneumatic 8 atm</String>
<String key="CplGangway">Passage</String>
<String key="CplAuxPneumatic">Air 8atm</String>
<String key="CplHeating">Heating</String>
<String key="CplWorkshop">Workshop coupling (uncouple lock)</String>
<String key="CplWorkshop">Lock</String>
<!-- Brake modes -->
<String key="BrakeFreight">Freight (G)</String>
<String key="BrakePassenger">Passenger (P)</String>
<String key="BrakeExpress">Express (R)</String>
<String key="BrakeExpressMg">Express + magnetic rail (R+Mg)</String>
<String key="BrakeNoAir">Start without air (Q)</String>
<String key="BrakeOff">Brakes disconnected (O)</String>
<String key="BrakeAuto">Automatic (A)</String>
<String key="BrakeActingNone">None</String>
<String key="BrakeFreight">G - Cargo</String>
<String key="BrakePassenger">P - Passenger</String>
<String key="BrakeExpress">R - Rapid</String>
<String key="BrakeExpressMg">R + magn. rail brake</String>
<String key="BrakeNoAir">Q - without air</String>
<!-- Load adaptation -->
<String key="LoadEmpty">Empty (T)</String>
<String key="LoadMedium">Loaded I — medium (H)</String>
<String key="LoadFull">Loaded II (F)</String>
<String key="LoadAuto">Automatic (A)</String>
<String key="LoadNeutral">N - neutral</String>
<String key="LoadEmpty">T - empty</String>
<String key="LoadMedium">H - loaded I</String>
<String key="LoadFull">F - loaded II</String>
<!-- Brake switch -->
<String key="SwitchOff">Off (0)</String>
<String key="SwitchOff10">Off with 10% chance (1)</String>
<String key="SwitchOn">On (A)</String>
<String key="SwitchNormal">N - On</String>
<String key="SwitchOff">0 - Off, slack</String>
<String key="SwitchOff10">1 - Off, slack (10% chance)</String>
<!-- Damage (wheel parameters) -->
<String key="Damage">Damage</String>
@@ -192,6 +195,9 @@
<String key="LoadType">Load type</String>
<String key="LoadCount">Amount</String>
<String key="LoadHint">Set an amount above 0 to load the vehicle.</String>
<String key="LoadNotLoadable">This vehicle accepts no load.</String>
<String key="LoadPantState">Pantograph raised at start</String>
<String key="LoadPantStateHint">The amount selects the pantograph: 1 — A, 2 — B, 3 — both.</String>
<String key="WagonNumber">Wagon number</String>
<String key="LoadCopyPrev">Copy load from previous vehicle</String>
<String key="LoadMax">Maximum possible load</String>
@@ -206,7 +212,7 @@
<String key="VehicleCount">Vehicles</String>
<String key="Track">Track</String>
<String key="EmptyTrainset">No vehicles. (Track: {0})</String>
<String key="HideArchivalVehicles">Hide archival vehicles</String>
<String key="HideArchivalVehicles">Hide archived vehicles</String>
<String key="RemoveSelectedVehicle">Remove vehicle</String>
<String key="RandomizeTextures">Randomize textures</String>
<String key="TipRandomizeTextures">Pick similar textures by class / rules</String>
@@ -242,6 +248,10 @@
<String key="ComingSoon">Editing options coming soon.</String>
<String key="Split">Split</String>
<String key="TipSplit">Split the unit into individual cars</String>
<String key="AppliesToUnit">Shared by the whole unit</String>
<String key="CouplingUnitRear">Coupler at the rear of the unit</String>
<String key="Join">Join</String>
<String key="TipJoin">Couple this vehicle with the next one into a single unit</String>
<String key="AddVehicle">Add</String>
<String key="TipAddVehicle">Add this vehicle to the consist</String>
<String key="TipAddUnit">Add the whole unit to the consist</String>
@@ -320,10 +330,28 @@
<String key="MaxTexCabResolution">Maximum cabin texture resolution:</String>
<String key="TexFilteringQuality">Texture filtering quality:</String>
<String key="Multisampling">Multisampling:</String>
<String key="QualityVeryLow">Very low</String>
<String key="QualityLow">Low</String>
<String key="QualityNormal">Normal</String>
<String key="QualityHigh">High</String>
<String key="QualityVeryHigh">Very high</String>
<String key="MsaaNone">None</String>
<String key="QualityModerate">Moderate</String>
<String key="QualityStandard">Standard</String>
<String key="RangeVeryLow">Very low</String>
<String key="RangeLow">Low</String>
<String key="RangeNormal">Normal</String>
<String key="RangeModerate">Moderate</String>
<String key="RangeStandard">Standard</String>
<String key="RangeHigh">High</String>
<String key="RangeVeryHigh">Very high</String>
<String key="RangeDisabled">Disabled</String>
<String key="DynamicLights">Dynamic light sources:</String>
<String key="RenderRange">Rendering range:</String>
<String key="FpsLimitEnable">Limit frame rate</String>
<String key="ShadowAngleLimit">Shadow length limit</String>
<String key="ShadowLimitNone">No limit</String>
<String key="ShadowLimitShort">Shortened</String>
<String key="FullscreenWindowed">Automatic resolution (borderless fullscreen)</String>
<String key="RenderAngleVulkan">Use ANGLE Vulkan renderer</String>
<String key="VSync">VSync</String>
@@ -427,4 +455,100 @@
<String key="Save">Save</String>
<String key="Reset">Reset</String>
<String key="SettingsSaved">Settings saved</String>
<!-- Errors / diagnostics -->
<String key="FaultTitle">Error</String>
<String key="FaultDetail">Error details:</String>
<String key="FaultFileNotFound">File not found: {0}</String>
<String key="FaultLoadData">Failed to load simulator data.</String>
<String key="FaultBadInstall">The simulator installation may be incomplete.</String>
<String key="FaultNoDir">Missing folder {0}</String>
<String key="FaultNoWeights">No load weight information.</String>
<String key="FaultNoScenery">No scenarios found.</String>
<String key="FaultNoVehicles">No vehicles found.</String>
<String key="FaultNoPhysics">No rolling stock data found.</String>
<String key="FaultNoExe">Simulator executable not found.</String>
<!-- About -->
<String key="AboutTitle">About</String>
<String key="AboutForSimulator">for the MaSzyna simulator</String>
<String key="AboutChangelog">Changelog:</String>
<String key="AboutNoChangelog">No changelog file found.</String>
<String key="About">About</String>
<String key="Close">Close</String>
<String key="SettingsPath">Settings loaded from:</String>
<!-- Vehicle info -->
<String key="GeneralInfo">General</String>
<String key="Model">Model</String>
<String key="VMax">VMax</String>
<String key="Texture">Texture</String>
<!-- Scenery tab -->
<String key="SceneryConsists">Consists on the scenery</String>
<String key="SceneryFaults">Scenario faults:</String>
<!-- Warehouse / vehicle panels -->
<String key="WarehouseConsists">Consists in the warehouse</String>
<String key="AddToWarehouse">Add to warehouse</String>
<String key="RemoveFromWarehouse">Remove from warehouse</String>
<String key="WarehouseApply">Replace with warehouse consist</String>
<String key="WarehouseImportNothing">No consists found to import.</String>
<String key="AboutTexture">About texture</String>
<String key="NoTextureInfo">No texture information.</String>
<!-- Vehicle configuration / coupler -->
<String key="Configuration">Configuration</String>
<String key="RemoveVehicle">Remove vehicle</String>
<String key="AutoCoupler">Auto</String>
<String key="TipAutoCoupler">Pick couplings automatically for the whole consist</String>
<String key="CouplingLastVehicle">Rear coupler of the train — no vehicle behind it.</String>
<String key="ChangeName">Rename...</String>
<String key="LoadCopyToAll">Apply this load to every following vehicle that accepts it</String>
<String key="WarehouseAppend">Append warehouse consist</String>
<String key="WarehouseImportFile">Add consists from file...</String>
<String key="StartWithoutSaving">Start without saving settings</String>
<String key="StartFreeFly">Start with no vehicle</String>
<String key="VolumePaused">Volume while paused:</String>
<String key="DebugLogVisible">Show simulation log on screen</String>
<String key="PyScreenPriority">Extra screen refresh rate:</String>
<String key="PyPriorityNormal">Standard</String>
<String key="PyPriorityLower">Low</String>
<String key="PyPriorityLowest">Very low</String>
<String key="PyPriorityIdle">None</String>
<String key="PyPriorityOff">Disabled</String>
<String key="MassBrutto">Gross mass</String>
<String key="VisibilityGood">Good</String>
<String key="VisibilityBad">Poor</String>
<String key="SoundPhysics">Sound | Physics</String>
<String key="DriverHead">Driver (cab A)</String>
<String key="DriverRear">Driver (cab B)</String>
<String key="DriverPassenger">Passenger</String>
<String key="DriverNobody">Nobody</String>
<String key="RemoveAllTrains">Remove all consists from the scenery</String>
<String key="RemoveAllTrainsConfirm">Remove the vehicles from every consist on this scenery?</String>
<String key="TexWorks">Overhaul:</String>
<String key="RevisionFrom">Overhaul from:</String>
<String key="RevisionTo">Overhaul to:</String>
<String key="CrewLabel">Crew:</String>
<String key="RuleAdd">Add to rule</String>
<String key="RuleNew">— new rule —</String>
<String key="RuleRemove">Remove from rule</String>
<String key="CatAll">All</String>
<String key="GfxColResolution">Resolution and textures</String>
<String key="GfxColRendering">Rendering</String>
<String key="GfxColShadows">Shadows</String>
<String key="GfxColReflections">Reflections and view</String>
<String key="GfxColScripts">Python scripts</String>
<String key="UartHint">Port name / leave alone / data send rate</String>
<String key="UartFeature">UARTfeature:</String>
<String key="UartSaveClose">Save and close</String>
<String key="PickCategoryFirst">Pick a vehicle category or class first.</String>
<String key="HideArchivalSceneries">Hide archived scenarios</String>
<String key="Tools">Tools</String>
<String key="AdvancedSettings">Advanced settings</String>
<String key="AdvLogPage">Event log</String>
<String key="AdvClearLog">Clear log</String>
<String key="ConsistSchedule">Composition</String>
</Language>

View File

@@ -33,6 +33,8 @@
<String key="Time">Idő</String>
<String key="CurrentTime">Most</String>
<String key="Day">Az év napja</String>
<String key="Date">Dátum</String>
<String key="DayOfYear">{0}. nap</String>
<String key="Sky">Égbolt</String>
<String key="Fog">Köd / látótávolság</String>
<String key="Season">Évszak</String>
@@ -44,6 +46,7 @@
<String key="SeasonSummer">Nyár</String>
<String key="SeasonAutumn">Ősz</String>
<String key="SeasonWinter">Tél</String>
<String key="OvercastRandom">Véletlenszerű</String>
<String key="OvercastClear">Derült</String>
<String key="OvercastFair">Gyengén felhős</String>
<String key="OvercastLight">Kevés felhő</String>
@@ -51,7 +54,8 @@
<String key="OvercastHeavy">Erős felhőzet</String>
<String key="OvercastCloudy">Borult</String>
<String key="OvercastStorm">Vihar</String>
<String key="WeatherHint">Az időjárás módosításai indításkor a pályába íródnak.</String>
<String key="WeatherDateTime">Dátum és idő</String>
<String key="WeatherConditions">Időjárási viszonyok</String>
<String key="Consist">Szerelvény</String>
<String key="Start">Indítás</String>
<String key="StartNoScenery">Válasszon pályát</String>
@@ -148,40 +152,39 @@
<String key="Coupling">Kapcsolókészülék</String>
<String key="MapConsists">A pálya szerelvényei</String>
<String key="NoSceneryConsists">Válasszon pályát a Forgatókönyvek fülön.</String>
<String key="BrakeMode">Fékmód</String>
<String key="BrakeLoad">Terhelési beállítás</String>
<String key="BrakeMode">Fékbeállítás</String>
<String key="BrakeLoad">Terhelésbeállítás</String>
<String key="BrakeSwitch">Fékkapcsoló</String>
<String key="None">—</String>
<!-- Coupling mask bits -->
<String key="CplMechanical">Mechanikus</String>
<String key="CplBrake">Pneumatikus 5 atm (fékek)</String>
<String key="CplControl">Több egység vezérlése</String>
<String key="CplBrake">Fék</String>
<String key="CplControl">Vezérlés</String>
<String key="CplHighVoltage">Nagyfeszültség</String>
<String key="CplGangway">Átjáró a járművek között</String>
<String key="CplAuxPneumatic">Segéd pneumatikus 8 atm</String>
<String key="CplGangway">Átjáró</String>
<String key="CplAuxPneumatic">Levegő 8atm</String>
<String key="CplHeating">Fűtés</String>
<String key="CplWorkshop">Műhelykapcsolás (szétkapcsolás tiltása)</String>
<String key="CplWorkshop">Zár</String>
<!-- Brake modes -->
<String key="BrakeFreight">Teher (G)</String>
<String key="BrakePassenger">Személy (P)</String>
<String key="BrakeExpress">Gyors (R)</String>
<String key="BrakeExpressMg">Gyors + mágneses sínfék (R+Mg)</String>
<String key="BrakeNoAir">Indítás levegő nélkül (Q)</String>
<String key="BrakeOff">Fékek lekapcsolva (O)</String>
<String key="BrakeAuto">Automatikus (A)</String>
<String key="BrakeActingNone">Nincs</String>
<String key="BrakeFreight">G - teher</String>
<String key="BrakePassenger">P - személy</String>
<String key="BrakeExpress">R - gyors</String>
<String key="BrakeExpressMg">R + mágneses sínfék</String>
<String key="BrakeNoAir">Q - levegő nélkül</String>
<!-- Load adaptation -->
<String key="LoadEmpty">Üres (T)</String>
<String key="LoadMedium">Terhelt I — közepes (H)</String>
<String key="LoadFull">Terhelt II (F)</String>
<String key="LoadAuto">Automatikus (A)</String>
<String key="LoadNeutral">N - semleges</String>
<String key="LoadEmpty">T - üres</String>
<String key="LoadMedium">H - terhelt I</String>
<String key="LoadFull">F - terhelt II</String>
<!-- Brake switch -->
<String key="SwitchOff">Ki (0)</String>
<String key="SwitchOff10">Ki 10% eséllyel (1)</String>
<String key="SwitchOn">Be (A)</String>
<String key="SwitchNormal">N - fék bekapcsolva</String>
<String key="SwitchOff">0 - kikapcsolva, oldva</String>
<String key="SwitchOff10">1 - kikapcsolva, oldva (10%)</String>
<!-- Damage (wheel parameters) -->
<String key="Damage">Sérülés</String>
@@ -192,6 +195,9 @@
<String key="LoadType">Rakomány típusa</String>
<String key="LoadCount">Mennyiség</String>
<String key="LoadHint">A jármű megrakásához adjon meg 0-nál nagyobb mennyiséget.</String>
<String key="LoadNotLoadable">Ez a jármű nem fogad rakományt.</String>
<String key="LoadPantState">Felemelt áramszedő induláskor</String>
<String key="LoadPantStateHint">A mennyiség az áramszedőt választja ki: 1 — A, 2 — B, 3 — mindkettő.</String>
<String key="WagonNumber">Kocsiszám</String>
<String key="LoadCopyPrev">Rakomány másolása az előző járműről</String>
<String key="LoadMax">Legnagyobb lehetséges rakomány</String>
@@ -206,7 +212,7 @@
<String key="VehicleCount">Járművek</String>
<String key="Track">Vágány</String>
<String key="EmptyTrainset">Nincs jármű. (Vágány: {0})</String>
<String key="HideArchivalVehicles">Archív járművek elrejtése</String>
<String key="HideArchivalVehicles">Archivált járművek elrejtése</String>
<String key="RemoveSelectedVehicle">Jármű eltávolítása</String>
<String key="RandomizeTextures">Textúrák sorsolása</String>
<String key="TipRandomizeTextures">Hasonló textúrák választása osztály / szabályok szerint</String>
@@ -242,6 +248,10 @@
<String key="ComingSoon">A szerkesztési lehetőségek hamarosan elérhetők.</String>
<String key="Split">Szétbontás</String>
<String key="TipSplit">Az egység szétbontása külön kocsikra</String>
<String key="AppliesToUnit">Az egész egységre közös</String>
<String key="CouplingUnitRear">Az egység végén lévő kapcsolókészülék</String>
<String key="Join">Összekapcsolás</String>
<String key="TipJoin">A jármű összekapcsolása a következővel egy egységgé</String>
<String key="AddVehicle">Hozzáadás</String>
<String key="TipAddVehicle">Jármű hozzáadása a szerelvényhez</String>
<String key="TipAddUnit">A teljes egység hozzáadása a szerelvényhez</String>
@@ -320,10 +330,28 @@
<String key="MaxTexCabResolution">Vezetőállás-textúrák legnagyobb felbontása:</String>
<String key="TexFilteringQuality">Textúraszűrés minősége:</String>
<String key="Multisampling">Multisampling:</String>
<String key="QualityVeryLow">Nagyon alacsony</String>
<String key="QualityLow">Alacsony</String>
<String key="QualityNormal">Normál</String>
<String key="QualityHigh">Magas</String>
<String key="QualityVeryHigh">Nagyon magas</String>
<String key="MsaaNone">Nincs</String>
<String key="QualityModerate">Közepes</String>
<String key="QualityStandard">Standard</String>
<String key="RangeVeryLow">Nagyon alacsony</String>
<String key="RangeLow">Alacsony</String>
<String key="RangeNormal">Normál</String>
<String key="RangeModerate">Közepes</String>
<String key="RangeStandard">Standard</String>
<String key="RangeHigh">Magas</String>
<String key="RangeVeryHigh">Nagyon magas</String>
<String key="RangeDisabled">Kikapcsolva</String>
<String key="DynamicLights">Dinamikus fényforrások:</String>
<String key="RenderRange">Megjelenítési távolság:</String>
<String key="FpsLimitEnable">Képkockaszám korlátozása</String>
<String key="ShadowAngleLimit">Árnyékhossz korlátozása</String>
<String key="ShadowLimitNone">Nincs korlát</String>
<String key="ShadowLimitShort">Rövidített</String>
<String key="FullscreenWindowed">Automatikus felbontás (keret nélküli teljes képernyő)</String>
<String key="RenderAngleVulkan">ANGLE Vulkan megjelenítő használata</String>
<String key="VSync">VSync</String>
@@ -427,4 +455,100 @@
<String key="Save">Mentés</String>
<String key="Reset">Visszaállítás</String>
<String key="SettingsSaved">A beállítások elmentve</String>
<!-- Errors / diagnostics -->
<String key="FaultTitle">Hiba</String>
<String key="FaultDetail">Hiba részletei:</String>
<String key="FaultFileNotFound">A fájl nem található: {0}</String>
<String key="FaultLoadData">A szimulátor adatainak betöltése sikertelen.</String>
<String key="FaultBadInstall">A szimulátor telepítése hiányos lehet.</String>
<String key="FaultNoDir">Hiányzó mappa: {0}</String>
<String key="FaultNoWeights">Nincs rakománysúly-információ.</String>
<String key="FaultNoScenery">Nem található forgatókönyv.</String>
<String key="FaultNoVehicles">Nem található jármű.</String>
<String key="FaultNoPhysics">Nem található járműadat.</String>
<String key="FaultNoExe">A szimulátor futtatható fájlja nem található.</String>
<!-- About -->
<String key="AboutTitle">Névjegy</String>
<String key="AboutForSimulator">a MaSzyna szimulátorhoz</String>
<String key="AboutChangelog">Változásnapló:</String>
<String key="AboutNoChangelog">Nem található változásnapló fájl.</String>
<String key="About">Névjegy</String>
<String key="Close">Bezárás</String>
<String key="SettingsPath">Beállítások betöltve innen:</String>
<!-- Vehicle info -->
<String key="GeneralInfo">Általános</String>
<String key="Model">Modell</String>
<String key="VMax">VMax</String>
<String key="Texture">Textúra</String>
<!-- Scenery tab -->
<String key="SceneryConsists">Szerelvények a pályán</String>
<String key="SceneryFaults">Hibák a forgatókönyvben:</String>
<!-- Warehouse / vehicle panels -->
<String key="WarehouseConsists">Szerelvények a raktárban</String>
<String key="AddToWarehouse">Hozzáadás a raktárhoz</String>
<String key="RemoveFromWarehouse">Eltávolítás a raktárból</String>
<String key="WarehouseApply">Csere raktári szerelvényre</String>
<String key="WarehouseImportNothing">Nem található importálható szerelvény.</String>
<String key="AboutTexture">A textúráról</String>
<String key="NoTextureInfo">Nincs textúrainformáció.</String>
<!-- Vehicle configuration / coupler -->
<String key="Configuration">Konfiguráció</String>
<String key="RemoveVehicle">Jármű eltávolítása</String>
<String key="AutoCoupler">Auto</String>
<String key="TipAutoCoupler">Kapcsolatok automatikus beállítása az egész szerelvényre</String>
<String key="CouplingLastVehicle">A szerelvény végén lévő kapcsolókészülék — nincs mögötte jármű.</String>
<String key="ChangeName">Átnevezés...</String>
<String key="LoadCopyToAll">A rakomány alkalmazása minden további járműre, amely elfogadja</String>
<String key="WarehouseAppend">Raktári szerelvény hozzáfűzése</String>
<String key="WarehouseImportFile">Szerelvények hozzáadása fájlból...</String>
<String key="StartWithoutSaving">Indítás a beállítások mentése nélkül</String>
<String key="StartFreeFly">Indítás jármű nélkül</String>
<String key="VolumePaused">Hangerő szünet alatt:</String>
<String key="DebugLogVisible">Szimuláció menetének megjelenítése</String>
<String key="PyScreenPriority">Kiegészítő képernyők frissítési gyakorisága:</String>
<String key="PyPriorityNormal">Normál</String>
<String key="PyPriorityLower">Alacsony</String>
<String key="PyPriorityLowest">Nagyon alacsony</String>
<String key="PyPriorityIdle">Nincs</String>
<String key="PyPriorityOff">Kikapcsolva</String>
<String key="MassBrutto">Bruttó tömeg</String>
<String key="VisibilityGood">Jó</String>
<String key="VisibilityBad">Rossz</String>
<String key="SoundPhysics">Hang | Fizika</String>
<String key="DriverHead">Mozdonyvezető (A fülke)</String>
<String key="DriverRear">Mozdonyvezető (B fülke)</String>
<String key="DriverPassenger">Utas</String>
<String key="DriverNobody">Senki</String>
<String key="RemoveAllTrains">Az összes szerelvény eltávolítása a pályáról</String>
<String key="RemoveAllTrainsConfirm">Eltávolítod a járműveket a pálya minden szerelvényéből?</String>
<String key="TexWorks">Felújítás:</String>
<String key="RevisionFrom">Felújítás ettől:</String>
<String key="RevisionTo">Felújítás eddig:</String>
<String key="CrewLabel">Személyzet:</String>
<String key="RuleAdd">Hozzáadás a szabályhoz</String>
<String key="RuleNew">— új szabály —</String>
<String key="RuleRemove">Eltávolítás a szabályból</String>
<String key="CatAll">Összes</String>
<String key="GfxColResolution">Felbontás és textúrák</String>
<String key="GfxColRendering">Megjelenítés</String>
<String key="GfxColShadows">Árnyékok</String>
<String key="GfxColReflections">Tükröződés és nézet</String>
<String key="GfxColScripts">Python szkriptek</String>
<String key="UartHint">Port neve / ne módosítsd / adatküldés gyakorisága</String>
<String key="UartFeature">UARTfeature:</String>
<String key="UartSaveClose">Mentés és bezárás</String>
<String key="PickCategoryFirst">Előbb válassz járműkategóriát vagy osztályt.</String>
<String key="HideArchivalSceneries">Archivált forgatókönyvek elrejtése</String>
<String key="Tools">Eszközök</String>
<String key="AdvancedSettings">Speciális beállítások</String>
<String key="AdvLogPage">Eseménynapló</String>
<String key="AdvClearLog">Napló törlése</String>
<String key="ConsistSchedule">Összeállítás</String>
</Language>

View File

@@ -33,6 +33,8 @@
<String key="Time">Godzina</String>
<String key="CurrentTime">Czas aktualny</String>
<String key="Day">Dzień roku</String>
<String key="Date">Data</String>
<String key="DayOfYear">dzień {0}</String>
<String key="Sky">Niebo</String>
<String key="Fog">Mgła / widoczność</String>
<String key="Season">Pora roku</String>
@@ -44,6 +46,7 @@
<String key="SeasonSummer">Lato</String>
<String key="SeasonAutumn">Jesień</String>
<String key="SeasonWinter">Zima</String>
<String key="OvercastRandom">Losowe</String>
<String key="OvercastClear">Bezchmurnie</String>
<String key="OvercastFair">Pogodnie</String>
<String key="OvercastLight">Lekkie zachmurzenie</String>
@@ -51,7 +54,8 @@
<String key="OvercastHeavy">Duże zachmurzenie</String>
<String key="OvercastCloudy">Pochmurno</String>
<String key="OvercastStorm">Burza</String>
<String key="WeatherHint">Zmiany pogody zostaną zapisane do scenerii przy uruchomieniu.</String>
<String key="WeatherDateTime">Data i godzina</String>
<String key="WeatherConditions">Warunki atmosferyczne</String>
<String key="Consist">Zestawienie</String>
<String key="Start">Uruchom</String>
<String key="StartNoScenery">Wybierz scenerię</String>
@@ -148,40 +152,39 @@
<String key="Coupling">Sprzęg</String>
<String key="MapConsists">Składy na scenerii</String>
<String key="NoSceneryConsists">Wybierz scenerię w zakładce Scenariusze.</String>
<String key="BrakeMode">Tryb hamulca</String>
<String key="BrakeLoad">Nastawa ładunkowa</String>
<String key="BrakeSwitch">Włącznik hamulca</String>
<String key="BrakeMode">Nastawa</String>
<String key="BrakeLoad">Dostosowanie</String>
<String key="BrakeSwitch">Wyłącznik</String>
<String key="None">—</String>
<!-- Bity maski sprzęgu -->
<String key="CplMechanical">Mechaniczny</String>
<String key="CplBrake">Pneumatyczny 5 atm (hamulce)</String>
<String key="CplControl">Ukrotnienie (sterowanie wielokrotne)</String>
<String key="CplMechanical">Mechanicznie</String>
<String key="CplBrake">Hamulec</String>
<String key="CplControl">Ukrotnienie</String>
<String key="CplHighVoltage">Wysokie napięcie</String>
<String key="CplGangway">Przejście między pojazdami</String>
<String key="CplAuxPneumatic">Pneumatyczny pomocniczy 8 atm</String>
<String key="CplGangway">Przejście</String>
<String key="CplAuxPneumatic">Powietrze 8atm</String>
<String key="CplHeating">Ogrzewanie</String>
<String key="CplWorkshop">Sprzęg warsztatowy (blokada rozłączania)</String>
<String key="CplWorkshop">Blokada</String>
<!-- Tryby nastawy hamulca -->
<String key="BrakeFreight">Towarowa (G)</String>
<String key="BrakePassenger">Osobowa (P)</String>
<String key="BrakeExpress">Pospieszna (R)</String>
<String key="BrakeExpressMg">Pospieszna + szynowy magnetyczny (R+Mg)</String>
<String key="BrakeNoAir">Start bez powietrza (Q)</String>
<String key="BrakeOff">Odłączone hamulce (O)</String>
<String key="BrakeAuto">Wybór automatyczny (A)</String>
<String key="BrakeActingNone">Brak</String>
<String key="BrakeFreight">G - nastawa towarowa</String>
<String key="BrakePassenger">P - nastawa pasażerska</String>
<String key="BrakeExpress">R - nastawa pospieszna</String>
<String key="BrakeExpressMg">R + magn. ham. szynowy</String>
<String key="BrakeNoAir">Q - bez powietrza</String>
<!-- Nastawa ładunkowa -->
<String key="LoadEmpty">Próżny (T)</String>
<String key="LoadMedium">Ładowny I — średni (H)</String>
<String key="LoadFull">Ładowny II (F)</String>
<String key="LoadAuto">Wybór automatyczny (A)</String>
<!-- Dostosowanie do ładunku -->
<String key="LoadNeutral">N - nastawienie neutralne</String>
<String key="LoadEmpty">T - próżny</String>
<String key="LoadMedium">H - ładowny I</String>
<String key="LoadFull">F - ładowny II</String>
<!-- Włącznik hamulca -->
<String key="SwitchOff">Wyłączony (0)</String>
<String key="SwitchOff10">Wyłączony z prawd. 10% (1)</String>
<String key="SwitchOn">Włączony (A)</String>
<!-- Wyłącznik hamulca -->
<String key="SwitchNormal">N - hamulec włączony</String>
<String key="SwitchOff">0 - wyłączony, odluźniony</String>
<String key="SwitchOff10">1 - wył., odl. z 10% szansą</String>
<!-- Uszkodzenia (parametry kół) -->
<String key="Damage">Uszkodzenia</String>
@@ -192,6 +195,9 @@
<String key="LoadType">Rodzaj ładunku</String>
<String key="LoadCount">Ilość</String>
<String key="LoadHint">Ustaw ilość większą od 0, aby załadować pojazd.</String>
<String key="LoadNotLoadable">Ten pojazd nie przyjmuje ładunku.</String>
<String key="LoadPantState">Pantograf podniesiony na starcie</String>
<String key="LoadPantStateHint">Ilość wybiera pantograf: 1 — A, 2 — B, 3 — oba.</String>
<String key="WagonNumber">Numer wagonu</String>
<String key="LoadCopyPrev">Kopiuj ładunek z poprzedniego pojazdu</String>
<String key="LoadMax">Maksymalny możliwy ładunek</String>
@@ -206,7 +212,7 @@
<String key="VehicleCount">Pojazdy</String>
<String key="Track">Tor</String>
<String key="EmptyTrainset">Wpis bez pojazdów. (Tor: {0})</String>
<String key="HideArchivalVehicles">Ukryj pojazdy archiwalne</String>
<String key="HideArchivalVehicles">Ukryj archiwalne pojazdy</String>
<String key="RemoveSelectedVehicle">Usuń pojazd</String>
<String key="RandomizeTextures">Losuj tekstury</String>
<String key="TipRandomizeTextures">Dobierz podobne tekstury według klasy / reguł</String>
@@ -242,6 +248,10 @@
<String key="ComingSoon">Opcje edycji wkrótce.</String>
<String key="Split">Rozdziel</String>
<String key="TipSplit">Rozdziel zespół na pojedyncze człony</String>
<String key="AppliesToUnit">Ustawienie wspólne dla całego zespołu</String>
<String key="CouplingUnitRear">Sprzęg na końcu zespołu</String>
<String key="Join">Złącz</String>
<String key="TipJoin">Połącz ten pojazd z następnym w jeden zespół</String>
<String key="AddVehicle">Dodaj</String>
<String key="TipAddVehicle">Dodaj ten pojazd do składu</String>
<String key="TipAddUnit">Dodaj cały zespół do składu</String>
@@ -320,10 +330,28 @@
<String key="MaxTexCabResolution">Maksymalna rozdzielczość tekstur kabiny:</String>
<String key="TexFilteringQuality">Jakość filtrowania tekstur:</String>
<String key="Multisampling">Multisampling:</String>
<String key="QualityVeryLow">Bardzo niska</String>
<String key="QualityLow">Niska</String>
<String key="QualityNormal">Normalna</String>
<String key="QualityHigh">Wysoka</String>
<String key="QualityVeryHigh">Bardzo wysoka</String>
<String key="MsaaNone">Brak</String>
<String key="QualityModerate">Umiarkowana</String>
<String key="QualityStandard">Standardowa</String>
<String key="RangeVeryLow">Bardzo niski</String>
<String key="RangeLow">Niski</String>
<String key="RangeNormal">Normalny</String>
<String key="RangeModerate">Umiarkowany</String>
<String key="RangeStandard">Standardowy</String>
<String key="RangeHigh">Wysoki</String>
<String key="RangeVeryHigh">Bardzo wysoki</String>
<String key="RangeDisabled">Wyłączone</String>
<String key="DynamicLights">Dynamiczne źródła światła:</String>
<String key="RenderRange">Zasięg renderowania:</String>
<String key="FpsLimitEnable">Ogranicz liczbę klatek na sekundę</String>
<String key="ShadowAngleLimit">Limit długości cienia</String>
<String key="ShadowLimitNone">Bez ograniczeń</String>
<String key="ShadowLimitShort">Skrócone</String>
<String key="FullscreenWindowed">Automatyczna rozdzielczość (pełny ekran bez obramowania)</String>
<String key="RenderAngleVulkan">Użyj renderera ANGLE Vulkan</String>
<String key="VSync">VSync</String>
@@ -427,4 +455,100 @@
<String key="Save">Zapisz</String>
<String key="Reset">Przywróć</String>
<String key="SettingsSaved">Zapisano ustawienia</String>
<!-- Errors / diagnostics -->
<String key="FaultTitle">Błąd</String>
<String key="FaultDetail">Szczegóły błędu:</String>
<String key="FaultFileNotFound">Nie znaleziono pliku: {0}</String>
<String key="FaultLoadData">Błąd wczytywania danych symulatora.</String>
<String key="FaultBadInstall">Możliwa błędna instalacja symulatora.</String>
<String key="FaultNoDir">Brak katalogu {0}</String>
<String key="FaultNoWeights">Brak informacji o wagach ładunków.</String>
<String key="FaultNoScenery">Nie znaleziono scenariuszy.</String>
<String key="FaultNoVehicles">Nie znaleziono pojazdów.</String>
<String key="FaultNoPhysics">Nie znaleziono danych taboru.</String>
<String key="FaultNoExe">Nie znaleziono pliku wykonywalnego symulatora.</String>
<!-- About -->
<String key="AboutTitle">O programie</String>
<String key="AboutForSimulator">dla symulatora MaSzyna</String>
<String key="AboutChangelog">Lista zmian:</String>
<String key="AboutNoChangelog">Brak pliku z listą zmian.</String>
<String key="About">O programie</String>
<String key="Close">Zamknij</String>
<String key="SettingsPath">Lokalizacja wczytanych ustawień:</String>
<!-- Vehicle info -->
<String key="GeneralInfo">Ogólne</String>
<String key="Model">Model</String>
<String key="VMax">VMax</String>
<String key="Texture">Tekstura</String>
<!-- Scenery tab -->
<String key="SceneryConsists">Składy na scenerii</String>
<String key="SceneryFaults">Błędy w scenariuszu:</String>
<!-- Warehouse / vehicle panels -->
<String key="WarehouseConsists">Składy w magazynie</String>
<String key="AddToWarehouse">Dodaj do magazynu</String>
<String key="RemoveFromWarehouse">Usuń z magazynu</String>
<String key="WarehouseApply">Zastąp składem z magazynu</String>
<String key="WarehouseImportNothing">Nie znaleziono składów do zaimportowania.</String>
<String key="AboutTexture">O teksturze</String>
<String key="NoTextureInfo">Brak informacji o teksturze.</String>
<!-- Vehicle configuration / coupler -->
<String key="Configuration">Konfiguracja</String>
<String key="RemoveVehicle">Usuń pojazd</String>
<String key="AutoCoupler">Auto</String>
<String key="TipAutoCoupler">Dobierz sprzęgi automatycznie dla całego składu</String>
<String key="CouplingLastVehicle">Sprzęg na końcu składu — nie ma za nim pojazdu.</String>
<String key="ChangeName">Zmień nazwę...</String>
<String key="LoadCopyToAll">Dodaj ten ładunek do wszystkich możliwych następnych pojazdów</String>
<String key="WarehouseAppend">Dołącz skład z magazynu</String>
<String key="WarehouseImportFile">Dodaj składy z pliku...</String>
<String key="StartWithoutSaving">Uruchom bez zapisu ustawień</String>
<String key="StartFreeFly">Uruchom bez pojazdu</String>
<String key="VolumePaused">Głośność podczas pauzy:</String>
<String key="DebugLogVisible">Wyświetlaj przebieg symulacji</String>
<String key="PyScreenPriority">Częstotliwość odświeżania ekranów dodatkowych:</String>
<String key="PyPriorityNormal">Standardowa</String>
<String key="PyPriorityLower">Niska</String>
<String key="PyPriorityLowest">Bardzo niska</String>
<String key="PyPriorityIdle">Brak</String>
<String key="PyPriorityOff">Wyłączona</String>
<String key="MassBrutto">Masa brutto</String>
<String key="VisibilityGood">Dobra</String>
<String key="VisibilityBad">Zła</String>
<String key="SoundPhysics">Dźwięk | Fizyka</String>
<String key="DriverHead">Maszynista (kabina A)</String>
<String key="DriverRear">Maszynista (kabina B)</String>
<String key="DriverPassenger">Pasażer</String>
<String key="DriverNobody">Nikt</String>
<String key="RemoveAllTrains">Usuń wszystkie składy ze scenerii</String>
<String key="RemoveAllTrainsConfirm">Usunąć pojazdy ze wszystkich składów na tej scenerii?</String>
<String key="TexWorks">Rewizja:</String>
<String key="RevisionFrom">Rewizja od:</String>
<String key="RevisionTo">Rewizja do:</String>
<String key="CrewLabel">Obsada:</String>
<String key="RuleAdd">Dodaj do reguły</String>
<String key="RuleNew">— nowa reguła —</String>
<String key="RuleRemove">Usuń z reguły</String>
<String key="CatAll">Wszystkie</String>
<String key="GfxColResolution">Rozdzielczość i tekstury</String>
<String key="GfxColRendering">Renderowanie</String>
<String key="GfxColShadows">Cienie</String>
<String key="GfxColReflections">Odbicia i widok</String>
<String key="GfxColScripts">Skrypty Python</String>
<String key="UartHint">Nazwa portu / nie zmieniać / częstotliwość wysyłania danych</String>
<String key="UartFeature">UARTfeature:</String>
<String key="UartSaveClose">Zapisz i zamknij</String>
<String key="PickCategoryFirst">Najpierw wybierz kategorię lub klasę pojazdów.</String>
<String key="HideArchivalSceneries">Ukryj scenariusze archiwalne</String>
<String key="Tools">Narzędzia</String>
<String key="AdvancedSettings">Ustawienia zaawansowane</String>
<String key="AdvLogPage">Dziennik zdarzeń</String>
<String key="AdvClearLog">Wyczyść log</String>
<String key="ConsistSchedule">Zestawienie</String>
</Language>

View File

@@ -33,6 +33,8 @@
<String key="Time">Время</String>
<String key="CurrentTime">Сейчас</String>
<String key="Day">День года</String>
<String key="Date">Дата</String>
<String key="DayOfYear">день {0}</String>
<String key="Sky">Небо</String>
<String key="Fog">Туман / видимость</String>
<String key="Season">Время года</String>
@@ -44,6 +46,7 @@
<String key="SeasonSummer">Лето</String>
<String key="SeasonAutumn">Осень</String>
<String key="SeasonWinter">Зима</String>
<String key="OvercastRandom">Случайно</String>
<String key="OvercastClear">Ясно</String>
<String key="OvercastFair">Малооблачно</String>
<String key="OvercastLight">Лёгкая облачность</String>
@@ -51,7 +54,8 @@
<String key="OvercastHeavy">Сильная облачность</String>
<String key="OvercastCloudy">Пасмурно</String>
<String key="OvercastStorm">Гроза</String>
<String key="WeatherHint">Изменения погоды записываются в сценерию при запуске.</String>
<String key="WeatherDateTime">Дата и время</String>
<String key="WeatherConditions">Погодные условия</String>
<String key="Consist">Состав</String>
<String key="Start">Пуск</String>
<String key="StartNoScenery">Выберите сценерию</String>
@@ -148,40 +152,39 @@
<String key="Coupling">Сцепка</String>
<String key="MapConsists">Составы на сценерии</String>
<String key="NoSceneryConsists">Выберите сценерию на вкладке Сценарии.</String>
<String key="BrakeMode">Режим торможения</String>
<String key="BrakeLoad">Режим загрузки</String>
<String key="BrakeSwitch">Переключатель тормоза</String>
<String key="BrakeMode">Установка тормоза</String>
<String key="BrakeLoad">Регулировка тормоза</String>
<String key="BrakeSwitch">Выключатель тормоза</String>
<String key="None">—</String>
<!-- Coupling mask bits -->
<String key="CplMechanical">Механическая</String>
<String key="CplBrake">Пневматическая 5 атм (тормоза)</String>
<String key="CplControl">Управление по системе многих единиц</String>
<String key="CplBrake">Тормоз</String>
<String key="CplControl">Управление</String>
<String key="CplHighVoltage">Высокое напряжение</String>
<String key="CplGangway">Переход между единицами</String>
<String key="CplAuxPneumatic">Вспомогательная пневматическая 8 атм</String>
<String key="CplGangway">Переход</String>
<String key="CplAuxPneumatic">Воздух 8атм</String>
<String key="CplHeating">Отопление</String>
<String key="CplWorkshop">Депотская сцепка (блокировка расцепления)</String>
<String key="CplWorkshop">Блокировка</String>
<!-- Brake modes -->
<String key="BrakeFreight">Грузовой (G)</String>
<String key="BrakePassenger">Пассажирский (P)</String>
<String key="BrakeExpress">Скорый (R)</String>
<String key="BrakeExpressMg">Скорый + магниторельсовый (R+Mg)</String>
<String key="BrakeNoAir">Пуск без воздуха (Q)</String>
<String key="BrakeOff">Тормоза отключены (O)</String>
<String key="BrakeAuto">Автоматически (A)</String>
<String key="BrakeActingNone">Нет</String>
<String key="BrakeFreight">G - грузовой режим</String>
<String key="BrakePassenger">P - пассажирский режим</String>
<String key="BrakeExpress">R - скорый режим</String>
<String key="BrakeExpressMg">R + магниторельсовый тормоз</String>
<String key="BrakeNoAir">Q - без воздуха</String>
<!-- Load adaptation -->
<String key="LoadEmpty">Порожний (T)</String>
<String key="LoadMedium">Гружёный I — средний (H)</String>
<String key="LoadFull">Гружёный II (F)</String>
<String key="LoadAuto">Автоматически (A)</String>
<String key="LoadNeutral">N - нейтральное положение</String>
<String key="LoadEmpty">T - порожний</String>
<String key="LoadMedium">H - гружёный I</String>
<String key="LoadFull">F - гружёный II</String>
<!-- Brake switch -->
<String key="SwitchOff">Выкл. (0)</String>
<String key="SwitchOff10">Выкл. с вероятностью 10 % (1)</String>
<String key="SwitchOn">Вкл. (A)</String>
<String key="SwitchNormal">N - тормоз включён</String>
<String key="SwitchOff">0 - выключен, отпущен</String>
<String key="SwitchOff10">1 - выключен, отпущен (10%)</String>
<!-- Damage (wheel parameters) -->
<String key="Damage">Повреждения</String>
@@ -192,6 +195,9 @@
<String key="LoadType">Вид груза</String>
<String key="LoadCount">Количество</String>
<String key="LoadHint">Задайте количество больше 0, чтобы загрузить единицу.</String>
<String key="LoadNotLoadable">Эта единица не принимает груз.</String>
<String key="LoadPantState">Поднятый пантограф на старте</String>
<String key="LoadPantStateHint">Количество выбирает пантограф: 1 — A, 2 — B, 3 — оба.</String>
<String key="WagonNumber">Номер вагона</String>
<String key="LoadCopyPrev">Копировать груз с предыдущей единицы</String>
<String key="LoadMax">Максимально возможный груз</String>
@@ -206,7 +212,7 @@
<String key="VehicleCount">Единиц</String>
<String key="Track">Путь</String>
<String key="EmptyTrainset">Нет единиц. (Путь: {0})</String>
<String key="HideArchivalVehicles">Скрыть архивные единицы</String>
<String key="HideArchivalVehicles">Скрыть архивный транспорт</String>
<String key="RemoveSelectedVehicle">Убрать единицу</String>
<String key="RandomizeTextures">Случайные текстуры</String>
<String key="TipRandomizeTextures">Подобрать похожие текстуры по классу / правилам</String>
@@ -242,6 +248,10 @@
<String key="ComingSoon">Параметры редактирования появятся позже.</String>
<String key="Split">Разделить</String>
<String key="TipSplit">Разделить сцеп на отдельные вагоны</String>
<String key="AppliesToUnit">Общее для всего состава</String>
<String key="CouplingUnitRear">Сцепка в конце состава</String>
<String key="Join">Соединить</String>
<String key="TipJoin">Соединить этот вагон со следующим в один состав</String>
<String key="AddVehicle">Добавить</String>
<String key="TipAddVehicle">Добавить эту единицу в состав</String>
<String key="TipAddUnit">Добавить весь сцеп в состав</String>
@@ -320,10 +330,28 @@
<String key="MaxTexCabResolution">Максимальное разрешение текстур кабины:</String>
<String key="TexFilteringQuality">Качество фильтрации текстур:</String>
<String key="Multisampling">Multisampling:</String>
<String key="QualityVeryLow">Очень низкое</String>
<String key="QualityLow">Низкое</String>
<String key="QualityNormal">Нормальное</String>
<String key="QualityHigh">Высокое</String>
<String key="QualityVeryHigh">Очень высокое</String>
<String key="MsaaNone">Нет</String>
<String key="QualityModerate">Умеренное</String>
<String key="QualityStandard">Стандартное</String>
<String key="RangeVeryLow">Очень низкий</String>
<String key="RangeLow">Низкий</String>
<String key="RangeNormal">Нормальный</String>
<String key="RangeModerate">Умеренный</String>
<String key="RangeStandard">Стандартный</String>
<String key="RangeHigh">Высокий</String>
<String key="RangeVeryHigh">Очень высокий</String>
<String key="RangeDisabled">Отключено</String>
<String key="DynamicLights">Динамические источники света:</String>
<String key="RenderRange">Дальность отрисовки:</String>
<String key="FpsLimitEnable">Ограничить частоту кадров</String>
<String key="ShadowAngleLimit">Ограничение длины теней</String>
<String key="ShadowLimitNone">Без ограничений</String>
<String key="ShadowLimitShort">Укороченные</String>
<String key="FullscreenWindowed">Автоматическое разрешение (полный экран без рамки)</String>
<String key="RenderAngleVulkan">Использовать рендерер ANGLE Vulkan</String>
<String key="VSync">VSync</String>
@@ -427,4 +455,100 @@
<String key="Save">Сохранить</String>
<String key="Reset">Сбросить</String>
<String key="SettingsSaved">Настройки сохранены</String>
<!-- Errors / diagnostics -->
<String key="FaultTitle">Ошибка</String>
<String key="FaultDetail">Подробности ошибки:</String>
<String key="FaultFileNotFound">Файл не найден: {0}</String>
<String key="FaultLoadData">Не удалось загрузить данные симулятора.</String>
<String key="FaultBadInstall">Возможно, симулятор установлен некорректно.</String>
<String key="FaultNoDir">Отсутствует папка {0}</String>
<String key="FaultNoWeights">Нет данных о массе грузов.</String>
<String key="FaultNoScenery">Сценарии не найдены.</String>
<String key="FaultNoVehicles">Транспортные средства не найдены.</String>
<String key="FaultNoPhysics">Данные подвижного состава не найдены.</String>
<String key="FaultNoExe">Исполняемый файл симулятора не найден.</String>
<!-- About -->
<String key="AboutTitle">О программе</String>
<String key="AboutForSimulator">для симулятора MaSzyna</String>
<String key="AboutChangelog">Список изменений:</String>
<String key="AboutNoChangelog">Файл со списком изменений не найден.</String>
<String key="About">О программе</String>
<String key="Close">Закрыть</String>
<String key="SettingsPath">Настройки загружены из:</String>
<!-- Vehicle info -->
<String key="GeneralInfo">Общие</String>
<String key="Model">Модель</String>
<String key="VMax">VMax</String>
<String key="Texture">Текстура</String>
<!-- Scenery tab -->
<String key="SceneryConsists">Составы на сценерии</String>
<String key="SceneryFaults">Ошибки в сценарии:</String>
<!-- Warehouse / vehicle panels -->
<String key="WarehouseConsists">Составы на складе</String>
<String key="AddToWarehouse">Добавить на склад</String>
<String key="RemoveFromWarehouse">Удалить со склада</String>
<String key="WarehouseApply">Заменить составом со склада</String>
<String key="WarehouseImportNothing">Составы для импорта не найдены.</String>
<String key="AboutTexture">О текстуре</String>
<String key="NoTextureInfo">Нет информации о текстуре.</String>
<!-- Vehicle configuration / coupler -->
<String key="Configuration">Конфигурация</String>
<String key="RemoveVehicle">Удалить транспорт</String>
<String key="AutoCoupler">Авто</String>
<String key="TipAutoCoupler">Подобрать сцепки автоматически для всего состава</String>
<String key="CouplingLastVehicle">Сцепка в конце состава — за ней нет вагона.</String>
<String key="ChangeName">Переименовать...</String>
<String key="LoadCopyToAll">Применить этот груз ко всем следующим подходящим транспортам</String>
<String key="WarehouseAppend">Присоединить состав со склада</String>
<String key="WarehouseImportFile">Добавить составы из файла...</String>
<String key="StartWithoutSaving">Запустить без сохранения настроек</String>
<String key="StartFreeFly">Запустить без транспорта</String>
<String key="VolumePaused">Громкость на паузе:</String>
<String key="DebugLogVisible">Показывать ход симуляции</String>
<String key="PyScreenPriority">Частота обновления дополнительных экранов:</String>
<String key="PyPriorityNormal">Стандартная</String>
<String key="PyPriorityLower">Низкая</String>
<String key="PyPriorityLowest">Очень низкая</String>
<String key="PyPriorityIdle">Нет</String>
<String key="PyPriorityOff">Отключена</String>
<String key="MassBrutto">Масса брутто</String>
<String key="VisibilityGood">Хорошая</String>
<String key="VisibilityBad">Плохая</String>
<String key="SoundPhysics">Звук | Физика</String>
<String key="DriverHead">Машинист (кабина A)</String>
<String key="DriverRear">Машинист (кабина B)</String>
<String key="DriverPassenger">Пассажир</String>
<String key="DriverNobody">Никто</String>
<String key="RemoveAllTrains">Удалить все составы со сценерии</String>
<String key="RemoveAllTrainsConfirm">Удалить транспорт из всех составов на этой сценерии?</String>
<String key="TexWorks">Ревизия:</String>
<String key="RevisionFrom">Ревизия с:</String>
<String key="RevisionTo">Ревизия по:</String>
<String key="CrewLabel">Экипаж:</String>
<String key="RuleAdd">Добавить в правило</String>
<String key="RuleNew">— новое правило —</String>
<String key="RuleRemove">Удалить из правила</String>
<String key="CatAll">Все</String>
<String key="GfxColResolution">Разрешение и текстуры</String>
<String key="GfxColRendering">Рендеринг</String>
<String key="GfxColShadows">Тени</String>
<String key="GfxColReflections">Отражения и вид</String>
<String key="GfxColScripts">Скрипты Python</String>
<String key="UartHint">Имя порта / не менять / частота отправки данных</String>
<String key="UartFeature">UARTfeature:</String>
<String key="UartSaveClose">Сохранить и закрыть</String>
<String key="PickCategoryFirst">Сначала выберите категорию или класс транспорта.</String>
<String key="HideArchivalSceneries">Скрыть архивные сценарии</String>
<String key="Tools">Инструменты</String>
<String key="AdvancedSettings">Расширенные настройки</String>
<String key="AdvLogPage">Журнал событий</String>
<String key="AdvClearLog">Очистить журнал</String>
<String key="ConsistSchedule">Состав</String>
</Language>