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/MainWindow.axaml.cs b/StarterNG/MainWindow.axaml.cs
index 5e46630..24cf4ae 100644
--- a/StarterNG/MainWindow.axaml.cs
+++ b/StarterNG/MainWindow.axaml.cs
@@ -1,25 +1,63 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Text;
using Avalonia.Controls;
using Avalonia.Interactivity;
-using SukiUI.Controls;
+using StarterNG.Classes;
+
namespace StarterNG;
-public partial class MainWindow : SukiWindow
+public partial class MainWindow : Window
{
public MainWindow()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
InitializeComponent();
+
+ // Extend the client area over the title bar so the logo + navigation sit
+ // on the top bar, while the OS caption buttons (min / max / close) stay in
+ // the top-right corner. The default chrome hints already keep the system
+ // caption buttons, so only the two supported hints are set here (this
+ // Avalonia build no longer exposes ExtendClientAreaChromeHints).
+ ExtendClientAreaToDecorationsHint = true;
+ // Match the thin caption strip (row 0 of the top bar): the OS min/max/close
+ // buttons fill this height at the top-right, sitting above the social row.
+ ExtendClientAreaTitleBarHeightHint = 32;
+
+ // Mirror the active language into the top-bar selector.
+ LangCombo.SelectedIndex = App.Loc.CurrentLanguage == "Polski" ? 0 : 1;
}
-
-
-
+
+ // ── Navigation: show the page whose RadioButton was just checked ──────────
+ private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not RadioButton { IsChecked: true } rb)
+ return;
+
+ // Guard against the controls not being created yet during initial load.
+ if (ScenariosView is null || DepotView is null || SettingsView is null)
+ return;
+
+ string page = rb.Tag as string ?? "scenarios";
+ ScenariosView.IsVisible = page == "scenarios";
+ DepotView.IsVisible = page == "depot";
+ SettingsView.IsVisible = page == "settings";
+ }
+
+ private void LangCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not ComboBox { SelectedItem: ComboBoxItem item }) return;
+ if (!item.IsKeyboardFocusWithin) return; // ignore the programmatic initial set
+ var lang = item.Content?.ToString();
+ if (!string.IsNullOrEmpty(lang))
+ App.ApplyLanguage(lang);
+ }
+
private void linkClick(object? sender, RoutedEventArgs e)
{
- if (sender is not Button btn)
+ if (sender is not Button btn)
return;
if (btn.Tag is not string url || string.IsNullOrWhiteSpace(url))
@@ -42,4 +80,53 @@ public partial class MainWindow : SukiWindow
});
}
}
-}
\ No newline at end of file
+
+ // ── START: export the (possibly depot-modified) scenery and launch the sim.
+ // Global on the main window, like the original Starter's bottom START button.
+ 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 = Path.GetDirectoryName(scenery.Path) ?? "scenery";
+ string exportName = "$" + Path.GetFileName(scenery.Path);
+ string exportPath = 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
+ Settings.Instance.CaptureAndSave();
+
+ // launch the game: -s $.scn -v
+ try
+ {
+ Process.Start(new ProcessStartInfo(Settings.Instance.ExecutablePath)
+ {
+ Arguments = $"-s {exportName} -v {vehicle}",
+ UseShellExecute = true
+ });
+ }
+ catch
+ {
+ return; // executable not found / not configured yet
+ }
+
+ // honour the "close starter automatically" preference
+ if (Settings.Instance.AutoCloseStarter)
+ Close();
+ }
+}
diff --git a/StarterNG/StarterNG.csproj b/StarterNG/StarterNG.csproj
index 25de13f..4117f8e 100644
--- a/StarterNG/StarterNG.csproj
+++ b/StarterNG/StarterNG.csproj
@@ -8,8 +8,8 @@
true
app.manifest
true
- 1.0.0.0
- 1.0.0.0
+ 1.0.0.1
+ 1.0.0.1
Starter
eu07.pl
@@ -32,7 +32,6 @@
-
diff --git a/StarterNG/Styles/Carbon.axaml b/StarterNG/Styles/Carbon.axaml
new file mode 100644
index 0000000..3309728
--- /dev/null
+++ b/StarterNG/Styles/Carbon.axaml
@@ -0,0 +1,190 @@
+
+
+
+
+ #15171B
+ #1E2327
+ #2A3036
+ #262C31
+ #177f00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StarterNG/Views/Depot.axaml b/StarterNG/Views/Depot.axaml
index 7bdfb63..d2ede6d 100644
--- a/StarterNG/Views/Depot.axaml
+++ b/StarterNG/Views/Depot.axaml
@@ -1,14 +1,13 @@
-
-
+
@@ -20,29 +19,47 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
+
@@ -54,11 +71,11 @@
DropDownOpened="SceneryConsistCombo_OnDropDownOpened"
SelectionChanged="SceneryConsistCombo_OnSelectionChanged" />
-
+
-
+
-
+
-
+
-
+
diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs
index c3b110e..b12964e 100644
--- a/StarterNG/Views/Depot.axaml.cs
+++ b/StarterNG/Views/Depot.axaml.cs
@@ -29,8 +29,7 @@ public partial class Depot : UserControl
// Shared, size-limited thumbnail cache (decoded down to display height).
private readonly Dictionary _miniCache = new();
- private const int BrowserThumbHeight = 38;
- private const int ConsistThumbHeight = 50;
+ private const int ConsistThumbHeight = 34;
private readonly Cursor _hand = new(StandardCursorType.Hand);
private int _nameCounter;
@@ -44,18 +43,21 @@ public partial class Depot : UserControl
// scenery-consist previews are built lazily on first dropdown open
private bool _consistPreviewsBuilt;
- // auto-expand search results only when there are few of them
- private const int AutoExpandLimit = 40;
- private const int MaxRowsPerGroup = 250;
-
// active database filters
- private Func? _categoryFilter; // matches a category letter, null = all
- private string? _classFilter; // group mini (e.g. EU07), null = all
+ private Func? _categoryFilter; // matches a category letter, null = none chosen
+ private string? _classFilter; // group mini (e.g. EU07), null = all in category
+
+ // the vehicle currently highlighted in the flat list (drives the mini preview
+ // and the single Add button)
+ private VehicleTexture? _browserSelected;
+
+ // hard cap on how many entries the flat list shows at once
+ private const int MaxListRows = 2000;
// card highlight brushes (theme-neutral overlays)
private static readonly IBrush CardBorder = new SolidColorBrush(Color.Parse("#33888888"));
- private static readonly IBrush CardBorderSel = new SolidColorBrush(Color.Parse("#AA4D8BFF"));
- private static readonly IBrush CardBgSel = new SolidColorBrush(Color.Parse("#224D8BFF"));
+ private static readonly IBrush CardBorderSel = new SolidColorBrush(Color.Parse("#AA41C400"));
+ private static readonly IBrush CardBgSel = new SolidColorBrush(Color.Parse("#2241C400"));
private static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080"));
// Vehicle types, keyed by the JSON "category" letter. Powered/technical
@@ -132,7 +134,7 @@ public partial class Depot : UserControl
_searchTimer.Tick += (_, _) =>
{
_searchTimer!.Stop();
- BuildBrowser();
+ BuildList();
};
// Defer population so the tab renders immediately, then fills in.
@@ -275,12 +277,13 @@ public partial class Depot : UserControl
private void PopulateCategoryCombo()
{
+ // No "All" entry: nothing is selected by default, so the list starts empty
+ // (matching the original Starter's depot).
_suppress = true;
categoryCombo.Items.Clear();
- categoryCombo.Items.Add(new ComboBoxItem { Content = App.Loc["All"], Tag = null });
foreach (var (locKey, match) in CategoryDefs)
categoryCombo.Items.Add(new ComboBoxItem { Content = App.Loc[locKey], Tag = match });
- categoryCombo.SelectedIndex = 0;
+ categoryCombo.SelectedIndex = -1;
_suppress = false;
_categoryFilter = null;
@@ -289,18 +292,16 @@ public partial class Depot : UserControl
private void RebuildClassCombo()
{
+ // No "All" entry: an unselected class means "every class in the category".
_suppress = true;
classCombo.Items.Clear();
- classCombo.Items.Add(new ComboBoxItem { Content = App.Loc["All"], Tag = null });
-
foreach (string cls in ClassesForCategory(_categoryFilter))
classCombo.Items.Add(new ComboBoxItem { Content = cls, Tag = cls });
-
- classCombo.SelectedIndex = 0;
+ classCombo.SelectedIndex = -1;
_suppress = false;
_classFilter = null;
- BuildBrowser();
+ BuildList();
}
private IEnumerable ClassesForCategory(Func? category) =>
@@ -322,87 +323,83 @@ public partial class Depot : UserControl
{
if (_suppress) return;
_classFilter = (classCombo.SelectedItem as ComboBoxItem)?.Tag as string;
- BuildBrowser();
+ BuildList();
}
// ----------------------------------------------------------- vehicle browser
- // Browser groups by the JSON "group" field, rendered as collapsed expanders.
- // Collapsed groups hold no controls, so the list stays light and smooth.
- private void BuildBrowser()
+ // Flat list of plain-text vehicle entries (no expanders, like the original
+ // Starter). Empty by default: an entry only appears once a category is chosen
+ // or a search is typed. Picking one drives the mini preview and Add button.
+ private void BuildList()
{
- browserStack.Children.Clear();
+ // reset selection-dependent UI
+ _browserSelected = null;
+ miniPreview.Source = null;
+ addVehicleButton.IsEnabled = false;
+
+ vehicleListBox.Items.Clear();
string search = searchBox.Text?.Trim() ?? "";
bool hasSearch = search.Length > 0;
- var matched = _db.Textures.Where(t => PassesFilters(t, search, hasSearch)).ToList();
+ // nothing selected and nothing searched -> keep the list empty
+ if (_categoryFilter == null && !hasSearch)
+ return;
- // Only auto-open groups when the search is specific. A broad search keeps
- // groups folded (just headers + counts) so we never realize thousands of
- // rows / decode thousands of bitmaps at once.
- bool autoExpand = hasSearch && matched.Count <= AutoExpandLimit;
+ var matched = _db.Textures
+ .Where(t => PassesFilters(t, search, hasSearch))
+ .OrderBy(t => t.TextureMini ?? t.Skinfile, StringComparer.OrdinalIgnoreCase)
+ .Take(MaxListRows)
+ .ToList();
- // fold by class (the group's mini)
- var grouped = matched
- .GroupBy(ClassOf)
- .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase);
-
- foreach (var group in grouped)
+ foreach (var texture in matched)
{
- var list = group
- .OrderBy(t => t.TextureMini ?? t.Skinfile, StringComparer.OrdinalIgnoreCase)
- .ToList();
-
- string header = string.IsNullOrEmpty(group.Key)
- ? App.Loc["Others"]
- : group.Key;
-
- var content = new StackPanel { Spacing = 2 };
- var expander = new Expander
+ vehicleListBox.Items.Add(new ListBoxItem
{
- Header = $"{header} ({list.Count})",
- IsExpanded = autoExpand, // folded by default
- HorizontalAlignment = HorizontalAlignment.Stretch
- };
- expander.Content = content;
-
- bool populated = false;
- void Populate()
- {
- if (populated) return;
- populated = true;
-
- int shown = Math.Min(list.Count, MaxRowsPerGroup);
- for (int i = 0; i < shown; i++)
- content.Children.Add(BuildBrowserItem(list[i]));
-
- if (list.Count > shown)
- content.Children.Add(new TextBlock
- {
- Text = $"… +{list.Count - shown}",
- Opacity = 0.6,
- Margin = new Thickness(6, 2)
- });
- }
-
- if (expander.IsExpanded)
- Populate();
- expander.Expanded += (_, _) => Populate();
-
- browserStack.Children.Add(expander);
- }
-
- if (browserStack.Children.Count == 0)
- {
- browserStack.Children.Add(new TextBlock
- {
- Text = App.Loc["NoVehicles"],
- Opacity = 0.6,
- TextWrapping = TextWrapping.Wrap,
- Margin = new Thickness(4)
+ Content = BrowserLabel(texture, _db.ResolveSet(texture)),
+ Tag = texture
});
}
+
+ if (vehicleListBox.Items.Count == 0)
+ vehicleListBox.Items.Add(new ListBoxItem
+ {
+ Content = App.Loc["NoVehicles"],
+ IsEnabled = false
+ });
+ }
+
+ // Selecting an entry shows its mini preview and enables the Add button.
+ private void VehicleListBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (vehicleListBox.SelectedItem is ListBoxItem { Tag: VehicleTexture texture })
+ {
+ _browserSelected = texture;
+ miniPreview.Source = GetMiniBitmap(_db.ResolveMiniName(texture), 54);
+ addVehicleButton.IsEnabled = true;
+ }
+ else
+ {
+ _browserSelected = null;
+ miniPreview.Source = null;
+ addVehicleButton.IsEnabled = false;
+ }
+ }
+
+ // Double-click an entry to replace the selected consist vehicle.
+ private void VehicleListBox_OnDoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
+ {
+ if (_browserSelected != null)
+ ReplaceSelected(_browserSelected);
+ }
+
+ // The single Add button inserts the highlighted vehicle after the selected
+ // consist vehicle (or at the end when nothing is selected).
+ private void AddVehicleButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (_browserSelected != null)
+ AddTexture(_browserSelected);
}
private bool PassesFilters(VehicleTexture t, string search, bool hasSearch)
@@ -428,54 +425,6 @@ public partial class Depot : UserControl
|| C(t.Meta?.Vehicle) || C(t.Meta?.Operator) || C(ClassOf(t));
}
- private Control BuildBrowserItem(VehicleTexture texture)
- {
- // Grid with a star label column keeps the label width constrained so it
- // wraps instead of overlapping the Add button.
- var grid = new Grid
- {
- Margin = new Thickness(2, 1),
- ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto")
- };
- ToolTip.SetTip(grid, texture.FullPath);
-
- var thumb = CreateLazyMini(_db.ResolveMiniName(texture), BrowserThumbHeight, 64);
- thumb.VerticalAlignment = VerticalAlignment.Center;
- Grid.SetColumn(thumb, 0);
-
- var set = _db.ResolveSet(texture);
- var label = new TextBlock
- {
- Text = BrowserLabel(texture, set),
- VerticalAlignment = VerticalAlignment.Center,
- FontSize = 12,
- TextWrapping = TextWrapping.Wrap,
- Margin = new Thickness(8, 0, 8, 0)
- };
- Grid.SetColumn(label, 1);
-
- var add = new Button
- {
- Content = App.Loc["AddVehicle"],
- FontSize = 11,
- Padding = new Thickness(10, 3),
- VerticalAlignment = VerticalAlignment.Center,
- Cursor = _hand
- };
- add.Classes.Add("Flat");
- ToolTip.SetTip(add, set != null ? App.Loc["TipAddUnit"] : App.Loc["TipAddVehicle"]);
- add.Click += (_, _) => AddTexture(texture);
- Grid.SetColumn(add, 2);
-
- grid.Children.Add(thumb);
- grid.Children.Add(label);
- grid.Children.Add(add);
-
- // double-click the row to replace the selected consist vehicle
- grid.DoubleTapped += (_, _) => ReplaceSelected(texture);
- return grid;
- }
-
// Replaces the selected consist unit with this vehicle/unit (keeping its
// slot, crew and orientation). Falls back to appending if nothing is selected.
private void ReplaceSelected(VehicleTexture texture)
@@ -506,29 +455,6 @@ public partial class Depot : UserControl
RebuildConsist();
}
- // An image that decodes its mini only while it is on screen, and releases it
- // when scrolled out of view, so an open group never holds every bitmap at once.
- private Image CreateLazyMini(string? miniName, int height, double width)
- {
- var img = new Image { Height = height, Width = width, Stretch = Stretch.Uniform };
- bool loaded = false;
- img.EffectiveViewportChanged += (_, e) =>
- {
- bool visible = e.EffectiveViewport.Width > 0 && e.EffectiveViewport.Height > 0;
- if (visible && !loaded)
- {
- img.Source = GetMiniBitmap(miniName, height);
- loaded = true;
- }
- else if (!visible && loaded)
- {
- img.Source = null;
- loaded = false;
- }
- };
- return img;
- }
-
private string BrowserLabel(VehicleTexture texture, IReadOnlyList? set)
{
string name = !string.IsNullOrEmpty(texture.TextureMini)
@@ -644,13 +570,13 @@ public partial class Depot : UserControl
foreach (var v in trainset.Vehicles)
{
- var bmp = GetMiniBitmap(_db.MiniForSkin(v.SkinFile) ?? v.MiniName, 28);
+ var bmp = GetMiniBitmap(_db.MiniForSkin(v.SkinFile) ?? v.MiniName, 20);
if (bmp != null)
- minis.Children.Add(new Image { Source = bmp, Height = 28, Stretch = Stretch.Uniform });
+ minis.Children.Add(new Image { Source = bmp, Height = 20, Stretch = Stretch.Uniform });
else
minis.Children.Add(new Border
{
- Height = 28, Width = 46, Background = Placeholder, CornerRadius = new CornerRadius(3)
+ Height = 20, Width = 32, Background = Placeholder, CornerRadius = new CornerRadius(3)
});
names.Children.Add(new TextBlock { Text = v.SkinFile, FontSize = 10, Opacity = 0.8 });
diff --git a/StarterNG/Views/Scenarios.axaml b/StarterNG/Views/Scenarios.axaml
index c8ae5ac..19ff694 100644
--- a/StarterNG/Views/Scenarios.axaml
+++ b/StarterNG/Views/Scenarios.axaml
@@ -1,68 +1,161 @@
-
+
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
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">
-
+
-
-
-
-
-
-
-
-
-
- Polski
- English
-
-
+
+
+
+
+
+
+
+
+ Polski
+ English
+
+
-
+
+
+
-
+
+
+
+
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ eu07.exe
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3840x2160
+ 2560x1440
+ 1920x1080
+ 1600x900
+ 1366x768
+ 1280x720
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- eu07.exe
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 3840x2160
- 2560x1440
- 1920x1080
- 1600x900
- 1366x768
- 1280x720
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StarterNG/starter/lang/en.xml b/StarterNG/starter/lang/en.xml
index e1c05d3..95cd93a 100644
--- a/StarterNG/starter/lang/en.xml
+++ b/StarterNG/starter/lang/en.xml
@@ -8,12 +8,40 @@
Depot
Settings
Controls
+ Help
+ Version:
Sceneries
Vehicles
Scenario description
Scenery description
+ Description
+ Weather
+ Timetable
+ No weather data in the scenery.
+ No timetable for the selected consist.
+ Time
+ Day of year
+ Sky
+ Fog / visibility
+ Season
+ Temperature
+ Visibility (fog)
+ Overcast
+ Today
+ Spring
+ Summer
+ Autumn
+ Winter
+ Clear
+ Fair
+ Light clouds
+ Moderate clouds
+ Heavy clouds
+ Cloudy
+ Storm
+ Weather changes are written to the scenery on launch.
Consist
Start
Open latest simulation log
@@ -57,7 +85,32 @@
Truck
People
Animals
- Wagons
+ Wagons A
+ Wagons B
+ Wagons C
+ Wagons D
+ Wagons E
+ Wagons F
+ Wagons G
+ Wagons H
+ Wagons I
+ Wagons J
+ Wagons K
+ Wagons L
+ Wagons M
+ Wagons N
+ Wagons O
+ Wagons P
+ Wagons Q
+ Wagons R
+ Wagons S
+ Wagons T
+ Wagons U
+ Wagons V
+ Wagons W
+ Wagons X
+ Wagons Y
+ Wagons Z
Other
Brakes
Loads
diff --git a/StarterNG/starter/lang/pl.xml b/StarterNG/starter/lang/pl.xml
index fd16ed0..cafef19 100644
--- a/StarterNG/starter/lang/pl.xml
+++ b/StarterNG/starter/lang/pl.xml
@@ -8,12 +8,40 @@
Zajezdnia
Ustawienia
Sterowanie
+ Pomoc
+ Wersja:
Scenerie
Pojazdy
Opis scenariusza
Opis scenerii
+ Opis
+ Pogoda
+ Rozkład jazdy
+ Brak danych o pogodzie w scenerii.
+ Brak rozkładu jazdy dla wybranego składu.
+ Godzina
+ Dzień roku
+ Niebo
+ Mgła / widoczność
+ Pora roku
+ Temperatura
+ Widoczność (mgła)
+ Zachmurzenie
+ Dziś
+ Wiosna
+ Lato
+ Jesień
+ Zima
+ Bezchmurnie
+ Pogodnie
+ Lekkie zachmurzenie
+ Umiarkowane zachmurzenie
+ Duże zachmurzenie
+ Pochmurno
+ Burza
+ Zmiany pogody zostaną zapisane do scenerii przy uruchomieniu.
Zestawienie
Uruchom
Otwórz ostatni log symulatora
@@ -57,7 +85,32 @@
Samochód ciężarowy
Ludzie
Zwierzęta
- Wagony
+ Wagony A
+ Wagony B
+ Wagony C
+ Wagony D
+ Wagony E
+ Wagony F
+ Wagony G
+ Wagony H
+ Wagony I
+ Wagony J
+ Wagony K
+ Wagony L
+ Wagony M
+ Wagony N
+ Wagony O
+ Wagony P
+ Wagony Q
+ Wagony R
+ Wagony S
+ Wagony T
+ Wagony U
+ Wagony V
+ Wagony W
+ Wagony X
+ Wagony Y
+ Wagony Z
Inne
Hamulce
Ładunki