From b977d224e91dcfae782f943729f15d99ed00c8ee Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 23 Jun 2026 18:20:09 +0200 Subject: [PATCH] Complete UI rebuild --- StarterNG/App.axaml | 24 +- StarterNG/Classes/Scenery.cs | 137 ++++++- StarterNG/MainWindow.axaml | 194 ++++----- StarterNG/MainWindow.axaml.cs | 101 ++++- StarterNG/StarterNG.csproj | 5 +- StarterNG/Styles/Carbon.axaml | 190 +++++++++ StarterNG/Views/Depot.axaml | 57 ++- StarterNG/Views/Depot.axaml.cs | 242 ++++------- StarterNG/Views/Scenarios.axaml | 191 ++++++--- StarterNG/Views/Scenarios.axaml.cs | 187 ++++++--- StarterNG/Views/Settings.axaml | 638 ++++++++++++----------------- StarterNG/starter/lang/en.xml | 55 ++- StarterNG/starter/lang/pl.xml | 55 ++- 13 files changed, 1322 insertions(+), 754 deletions(-) create mode 100644 StarterNG/Styles/Carbon.axaml diff --git a/StarterNG/App.axaml b/StarterNG/App.axaml index ba0d66c..a347f53 100644 --- a/StarterNG/App.axaml +++ b/StarterNG/App.axaml @@ -1,17 +1,33 @@ - + RequestedThemeVariant="Dark"> + - + + + + + #177f00 + #136B00 + #0F5600 + #0B4200 + #2A9A0F + #41C400 + #5FD42A + + + + + diff --git a/StarterNG/Classes/Scenery.cs b/StarterNG/Classes/Scenery.cs index e148f88..1c949d8 100644 --- a/StarterNG/Classes/Scenery.cs +++ b/StarterNG/Classes/Scenery.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -18,6 +19,21 @@ public class Scenery public string Description; // //$d - scenery description public string ImageName; // //$i - main-window image (scenery thumbnail) + // Weather / environment. Like the original Starter, these are editable and are + // written into the scenery's "config" block on launch (see RewriteWeather). + // Defaults mirror the original (15 °C, day 0, 10:30, clear sky). + public string WeatherTime = "10:30"; // h:mm -> "time"/"scenario.time.override" + public int Day = 0; // "movelight " (day of year / season) + public double Temperature = 15; // "scenario.weather.temperature" + public int FogEnd = 2000; // visibility in metres (atmo fog range) + public double Overcast = 0; // atmo overcast factor (-1.5 .. 1.5) + + /// True when the scenery actually declared any weather command. + public bool HasWeather; + + /// Set once the user edits the weather, so export rewrites the config. + public bool WeatherDirty; + // The file content with each trainset block replaced by a {{i}} placeholder, // used to rebuild the .scn on export. private readonly string _template; @@ -40,9 +56,14 @@ public class Scenery // matching //$it, etc. this.Group = MatchDirective(content, "l"); this.Name = MatchDirective(content, "n"); - this.Description = MatchDirective(content, "d"); + // //$d may appear on several lines; each is one line of the description. + this.Description = MatchAllDirectives(content, "d"); this.ImageName = MatchDirective(content, "i"); + // weather/environment is read from the raw text before trainset blocks + // are stripped out below (the atmosphere commands live outside trainsets) + ParseWeather(content); + // parsing trainsets List trainsetEntries = new List(); @@ -71,11 +92,125 @@ public class Scenery public string BuildExportContent() { string result = _template; + + // Inject the (edited) weather into the config block before substituting + // trainsets, so the {{i}} placeholders shield the trainset text from the + // weather rewrite. Only done when the user actually changed the weather. + if (WeatherDirty) + result = RewriteWeather(result); + for (int i = 0; i < Trainsets.Count; i++) result = result.Replace("{{" + i + "}}", Trainsets[i].ToSceneryEntry()); return result; } + /// + /// Reads the scenery's environment commands into the editable weather fields. + /// Mirrors the original Starter (config: movelight / scenario.weather.temperature + /// / scenario.time.override, plus top-level time and the atmo fog/overcast block). + /// + private void ParseWeather(string content) + { + // start time: "scenario.time.override h:mm" wins, else top-level "time h:mm" + var ovr = Regex.Match(content, @"(?i)scenario\.time\.override\s+(\d{1,2})[:.](\d{2})"); + var time = ovr.Success + ? ovr + : Regex.Match(content, @"(?im)^\s*time\s+(\d{1,2})[:.](\d{2})\b"); + if (time.Success) + { + WeatherTime = $"{time.Groups[1].Value.PadLeft(2, '0')}:{time.Groups[2].Value}"; + HasWeather = true; + } + + // "movelight " - day of the year (sun elevation / season) + var move = Regex.Match(content, @"(?im)\bmovelight\s+(-?\d+)"); + if (move.Success && int.TryParse(move.Groups[1].Value, out int day)) + { + Day = day; + HasWeather = true; + } + + // "scenario.weather.temperature <°C>" + var temp = Regex.Match(content, @"(?i)scenario\.weather\.temperature\s+(-?\d+(?:\.\d+)?)"); + if (temp.Success && + double.TryParse(temp.Groups[1].Value, System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out double t)) + { + Temperature = t; + HasWeather = true; + } + + // "atmo R G B fogStart fogEnd R G B overcast endatmo" + var atmo = Regex.Match(content, @"(?is)\batmo\b(.*?)\bendatmo\b"); + if (atmo.Success) + { + var nums = Regex.Matches(atmo.Groups[1].Value, @"-?\d+(?:\.\d+)?") + .Select(m => m.Value).ToList(); + if (nums.Count >= 5 && int.TryParse(nums[4], out int fog)) + { + FogEnd = fog; + HasWeather = true; + } + if (nums.Count >= 6 && + double.TryParse(nums[^1], System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out double oc)) + Overcast = oc; + } + } + + /// + /// Replaces the scenery's environment commands with a fresh config block built + /// from the current weather fields (the C# equivalent of the original Starter's + /// TLexParser.ChangeConfig). On any failure the original text is returned + /// unchanged so a launch is never blocked by a bad rewrite. + /// + private string RewriteWeather(string text) + { + try + { + // strip the existing weather commands + string s = text; + 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", " "); + s = Regex.Replace(s, @"(?im)^[ \t]*movelight\s+\S+", " "); + s = Regex.Replace(s, @"(?i)scenario\.weather\.temperature\s+\S+", " "); + s = Regex.Replace(s, @"(?i)scenario\.time\.override\s+\S+", " "); + + var inv = System.Globalization.CultureInfo.InvariantCulture; + string config = + "config\r\n" + + $"movelight {Day}\r\n" + + $"scenario.weather.temperature {Temperature.ToString(inv)}\r\n" + + $"scenario.time.override {WeatherTime}\r\n" + + $"time {WeatherTime} 0 0 endtime\r\n" + + $"atmo 0 0 0 {FogEnd} {FogEnd} 0 0 0 {Overcast.ToString(inv)} endatmo\r\n" + + "endconfig\r\n"; + + return config + s; + } + catch + { + return text; + } + } + + /// + /// Collects every //$<letter> line (e.g. all //$d lines) and joins them + /// into one multi-line string. Returns null when none are present. + /// + private static string MatchAllDirectives(string content, string letter) + { + var matches = Regex.Matches( + content, + @"^//\$" + letter + @"\b[ \t]*([^\r\n]*)", + RegexOptions.Multiline + ); + if (matches.Count == 0) + return null; + return string.Join("\n", matches.Select(m => m.Groups[1].Value.TrimEnd())); + } + /// /// Reads a single starter directive value (the text after //$<letter>). /// Returns null when the directive is absent. The trailing whitespace diff --git a/StarterNG/MainWindow.axaml b/StarterNG/MainWindow.axaml index 55e939b..5196d93 100644 --- a/StarterNG/MainWindow.axaml +++ b/StarterNG/MainWindow.axaml @@ -1,112 +1,116 @@ - - - - - - - - - - - - - - - + Title="Starter MaSzyna" + Width="1200" Height="768" MinWidth="1200" MinHeight="666" + WindowStartupLocation="CenterScreen"> + - - - - - - - - - + - - - - - - - - - - - - - - - - - - - + + + + + - + + + + - - - + + + + + + - - - - - - - - - - - - + + Polski + English + + + + + + + + + + + + + + + + diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs index adc9d5e..b8053e9 100644 --- a/StarterNG/Views/Scenarios.axaml.cs +++ b/StarterNG/Views/Scenarios.axaml.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -16,6 +17,11 @@ public partial class Scenarios : UserControl { public List Sceneries; + // scenery whose weather the form is currently editing + private Scenery? _currentScenery; + // suppresses weather change handlers while the form is being populated + private bool _loadingWeather; + public Scenarios() { InitializeComponent(); @@ -76,6 +82,11 @@ public partial class Scenarios : UserControl // scenery-level info (distinct from the per-consist scenario description) ShowSceneryInfo(selectedScn); + ShowWeather(selectedScn); + + // no consist chosen yet for this scenery + missionDescription.Text = ""; + timetableContent.Text = App.Loc["NoTimetable"]; // add all trainsets to list for (int i = 0; i < selectedScn.Trainsets.Count; i++) @@ -136,6 +147,135 @@ public partial class Scenarios : UserControl AppState.Instance.CurrentTrainset = selectedTrainset; ShowConsist(selectedTrainset); + ShowTimetable(selectedScn, selectedTrainset); + } + + // Loads the scenery's weather into the editable form (Weather tab). Editing a + // control writes the value back onto the scenery and marks it dirty, so the + // change is injected into the exported .scn's config block on launch. + private void ShowWeather(Scenery scenery) + { + _currentScenery = scenery; + _loadingWeather = true; + try + { + weatherForm.IsEnabled = true; + weatherTime.Text = scenery.WeatherTime; + weatherDay.Value = scenery.Day; + weatherTemp.Value = scenery.Temperature; + weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 50, 2500); + SelectByTag(weatherSeason, scenery.Day.ToString()); + SelectByTag(weatherOvercast, scenery.Overcast.ToString(CultureInfo.InvariantCulture)); + UpdateWeatherLabels(); + } + finally + { + _loadingWeather = false; + } + } + + // Reads the form back into the current scenery and flags it for export rewrite. + private void CaptureWeather() + { + if (_loadingWeather || _currentScenery is null) + return; + + var scn = _currentScenery; + if (!string.IsNullOrWhiteSpace(weatherTime.Text)) + scn.WeatherTime = weatherTime.Text!.Trim(); + scn.Day = (int)(weatherDay.Value ?? 0); + 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.WeatherDirty = true; + UpdateWeatherLabels(); + } + + private void UpdateWeatherLabels() + { + weatherTempLabel.Text = $"{(int)weatherTemp.Value} °C"; + weatherFogLabel.Text = $"{(int)weatherFog.Value} m"; + } + + private static void SelectByTag(ComboBox combo, string tag) + { + foreach (var obj in combo.Items) + if (obj is ComboBoxItem item && (item.Tag as string) == tag) + { + combo.SelectedItem = item; + return; + } + combo.SelectedItem = null; + } + + // --- weather form event handlers --- + private void Weather_OnChanged(object? sender, RoutedEventArgs e) => CaptureWeather(); + + private void Weather_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) => CaptureWeather(); + + private void Weather_OnSliderChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) => CaptureWeather(); + + private void WeatherDay_OnChanged(object? sender, NumericUpDownValueChangedEventArgs e) => CaptureWeather(); + + // Picking a season presets the day-of-year, then captures the form. + private void WeatherSeason_OnChanged(object? sender, SelectionChangedEventArgs e) + { + if (_loadingWeather) + return; + if ((weatherSeason.SelectedItem as ComboBoxItem)?.Tag is string tag && + int.TryParse(tag, out int day)) + { + _loadingWeather = true; + weatherDay.Value = day; + _loadingWeather = false; + } + CaptureWeather(); + } + + // Reads and shows the timetable file referenced by the trainset (its first + // "trainset" token is the timetable name). Shows a note when none is set or + // the file cannot be found. + private void ShowTimetable(Scenery scenery, Trainset trainset) + { + string? path = ResolveTimetablePath(scenery, trainset.Name); + if (path is null) + { + timetableContent.Text = App.Loc["NoTimetable"]; + return; + } + + try + { + timetableContent.Text = File.ReadAllText(path, Encoding.GetEncoding(1250)); + } + catch + { + timetableContent.Text = App.Loc["NoTimetable"]; + } + } + + // Probes the usual locations for a timetable file named after the trainset. + private static string? ResolveTimetablePath(Scenery scenery, string? name) + { + if (string.IsNullOrWhiteSpace(name) || + name.Equals("none", System.StringComparison.OrdinalIgnoreCase)) + return null; + + string scnDir = Path.GetDirectoryName(scenery.Path) ?? "."; // scenery/ + string root = Path.GetDirectoryName(scnDir) ?? "."; // MaSzyna root + + var candidates = new[] + { + Path.Combine(root, "timetables", name + ".txt"), + Path.Combine(scnDir, name + ".txt"), + Path.Combine(root, "scenario", name + ".txt"), + Path.Combine(root, "timetables", name), + Path.Combine(scnDir, name), + }; + return candidates.FirstOrDefault(File.Exists); } // Renders the consist preview for a trainset (re-reads its current vehicles @@ -156,7 +296,7 @@ public partial class Scenarios : UserControl consistStack.Children.Add(new Image { Source = new Bitmap(path), - Height = 45, + Height = 32, Stretch = Avalonia.Media.Stretch.Uniform, Margin = new Thickness(0) }); @@ -171,49 +311,4 @@ public partial class Scenarios : UserControl if (AppState.Instance.CurrentTrainset is { } trainset) ShowConsist(trainset); } - - // Exports the (possibly depot-modified) scenery to a $-prefixed copy and - // launches the game on it with the selected consist's driven vehicle. - private void StartButton_OnClick(object? sender, RoutedEventArgs e) - { - var scenery = AppState.Instance.CurrentScenery; - if (scenery is null) - return; - var trainset = AppState.Instance.CurrentTrainset; - - // write scenery/$.scn with the replaced trainsets - string dir = System.IO.Path.GetDirectoryName(scenery.Path) ?? "scenery"; - string exportName = "$" + System.IO.Path.GetFileName(scenery.Path); - string exportPath = System.IO.Path.Combine(dir, exportName); - try - { - File.WriteAllText(exportPath, scenery.BuildExportContent(), Encoding.GetEncoding(1250)); - } - catch - { - return; // couldn't write the scenery file - } - - // the player's vehicle is the node name of the driven car in the consist - string? vehicle = trainset?.Vehicles - .FirstOrDefault(v => v.DriverType is eDriverType.Headdriver or eDriverType.Reardriver)?.Name - ?? trainset?.Vehicles.FirstOrDefault()?.Name; - - // make sure the latest settings are on disk before the sim reads them - StarterNG.Classes.Settings.Instance.CaptureAndSave(); - - // launch the game: -s $.scn -v - try - { - Process.Start(new ProcessStartInfo(StarterNG.Classes.Settings.Instance.ExecutablePath) - { - Arguments = $"-s {exportName} -v {vehicle}", - UseShellExecute = true - }); - } - catch - { - // executable not found / not configured yet - } - } } diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index e6ae6ef..6ce02ff 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -2,15 +2,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:suki="clr-namespace:SukiUI.Controls;assembly=SukiUI" - xmlns:objectModel="clr-namespace:System.Collections.ObjectModel;assembly=System.ObjectModel" xmlns:starterNg="clr-namespace:StarterNG" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="StarterNG.Views.Settings" x:CompileBindings="True"> - +