mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-07-17 17:09:19 +02:00
consist editor improvements
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Markup.Xaml.Styling;
|
||||
using Avalonia.Threading;
|
||||
using StarterNG.Classes;
|
||||
using StarterNG.Services;
|
||||
|
||||
namespace StarterNG;
|
||||
@@ -54,7 +58,30 @@ public partial class App : Application
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
// Code page 1250 is needed to parse scenery files (done during load).
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
var splash = new SplashWindow();
|
||||
desktop.MainWindow = splash;
|
||||
splash.Show();
|
||||
|
||||
// Progress<T> marshals callbacks back to this (UI) thread.
|
||||
var progress = new Progress<LoadStatus>(splash.Report);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
GameData.Instance.Load(progress);
|
||||
await Task.Delay(400); // let the completed bar be visible briefly
|
||||
}).ContinueWith(_ =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var main = new MainWindow();
|
||||
desktop.MainWindow = main; // keep a main window before closing the splash
|
||||
main.Show();
|
||||
splash.Close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
|
||||
@@ -33,11 +33,36 @@
|
||||
<x:String x:Key="CatDMU">DMU</x:String>
|
||||
<x:String x:Key="CatCarriagesA">Carriages A</x:String>
|
||||
<x:String x:Key="CatCarriagesB">Carriages B</x:String>
|
||||
<x:String x:Key="CatElectricLoco">Electric locomotive</x:String>
|
||||
<x:String x:Key="CatDieselLoco">Diesel locomotive</x:String>
|
||||
<x:String x:Key="CatSteamLoco">Steam locomotive</x:String>
|
||||
<x:String x:Key="CatRailbus">Railbus</x:String>
|
||||
<x:String x:Key="CatDraisine">Draisine</x:String>
|
||||
<x:String x:Key="CatWork">Work vehicles</x:String>
|
||||
<x:String x:Key="CatPrototype">Prototype</x:String>
|
||||
<x:String x:Key="CatTram">Tram</x:String>
|
||||
<x:String x:Key="CatCar">Car</x:String>
|
||||
<x:String x:Key="CatBus">Bus</x:String>
|
||||
<x:String x:Key="CatTruck">Truck</x:String>
|
||||
<x:String x:Key="CatPeople">People</x:String>
|
||||
<x:String x:Key="CatAnimals">Animals</x:String>
|
||||
<x:String x:Key="CatWagons">Wagons</x:String>
|
||||
<x:String x:Key="CatOther">Other</x:String>
|
||||
<x:String x:Key="Brakes">Brakes</x:String>
|
||||
<x:String x:Key="Loads">Loads</x:String>
|
||||
<x:String x:Key="Coupling">Coupling</x:String>
|
||||
<x:String x:Key="SelectVehicleHint">Select a vehicle in the consist to edit it.</x:String>
|
||||
<x:String x:Key="ComingSoon">Editing options coming soon.</x:String>
|
||||
<x:String x:Key="Split">Split</x:String>
|
||||
<x:String x:Key="TipSplit">Split the unit into individual cars</x:String>
|
||||
<x:String x:Key="AddVehicle">Add</x:String>
|
||||
<x:String x:Key="TipAddVehicle">Add this vehicle to the consist</x:String>
|
||||
<x:String x:Key="TipAddUnit">Add the whole unit to the consist</x:String>
|
||||
|
||||
<!-- Loading splash -->
|
||||
<x:String x:Key="LoadingVehicles">Loading vehicle database</x:String>
|
||||
<x:String x:Key="LoadingSceneries">Loading sceneries</x:String>
|
||||
<x:String x:Key="LoadingDone">Starting…</x:String>
|
||||
|
||||
<!-- Settings -->
|
||||
<x:String x:Key="General">General</x:String>
|
||||
|
||||
@@ -33,11 +33,36 @@
|
||||
<x:String x:Key="CatDMU">SZT</x:String>
|
||||
<x:String x:Key="CatCarriagesA">Wagony A</x:String>
|
||||
<x:String x:Key="CatCarriagesB">Wagony B</x:String>
|
||||
<x:String x:Key="CatElectricLoco">Lokomotywa elektryczna</x:String>
|
||||
<x:String x:Key="CatDieselLoco">Lokomotywa spalinowa</x:String>
|
||||
<x:String x:Key="CatSteamLoco">Parowóz</x:String>
|
||||
<x:String x:Key="CatRailbus">Szynobus</x:String>
|
||||
<x:String x:Key="CatDraisine">Drezyna</x:String>
|
||||
<x:String x:Key="CatWork">Pojazdy robocze</x:String>
|
||||
<x:String x:Key="CatPrototype">Prototyp</x:String>
|
||||
<x:String x:Key="CatTram">Tramwaj</x:String>
|
||||
<x:String x:Key="CatCar">Samochód osobowy</x:String>
|
||||
<x:String x:Key="CatBus">Autobus</x:String>
|
||||
<x:String x:Key="CatTruck">Samochód ciężarowy</x:String>
|
||||
<x:String x:Key="CatPeople">Ludzie</x:String>
|
||||
<x:String x:Key="CatAnimals">Zwierzęta</x:String>
|
||||
<x:String x:Key="CatWagons">Wagony</x:String>
|
||||
<x:String x:Key="CatOther">Inne</x:String>
|
||||
<x:String x:Key="Brakes">Hamulce</x:String>
|
||||
<x:String x:Key="Loads">Ładunki</x:String>
|
||||
<x:String x:Key="Coupling">Sprzęg</x:String>
|
||||
<x:String x:Key="SelectVehicleHint">Wybierz pojazd w składzie, aby go edytować.</x:String>
|
||||
<x:String x:Key="ComingSoon">Opcje edycji wkrótce.</x:String>
|
||||
<x:String x:Key="Split">Rozdziel</x:String>
|
||||
<x:String x:Key="TipSplit">Rozdziel zespół na pojedyncze człony</x:String>
|
||||
<x:String x:Key="AddVehicle">Dodaj</x:String>
|
||||
<x:String x:Key="TipAddVehicle">Dodaj ten pojazd do składu</x:String>
|
||||
<x:String x:Key="TipAddUnit">Dodaj cały zespół do składu</x:String>
|
||||
|
||||
<!-- Ekran ładowania -->
|
||||
<x:String x:Key="LoadingVehicles">Wczytywanie bazy pojazdów</x:String>
|
||||
<x:String x:Key="LoadingSceneries">Wczytywanie scenerii</x:String>
|
||||
<x:String x:Key="LoadingDone">Uruchamianie…</x:String>
|
||||
|
||||
<!-- Ustawienia -->
|
||||
<x:String x:Key="General">Ogólne</x:String>
|
||||
|
||||
15
StarterNG/Classes/AppState.cs
Normal file
15
StarterNG/Classes/AppState.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace StarterNG.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Shared selection state across views: the scenery and trainset currently in
|
||||
/// focus. The depot edits this trainset in place, so changes made in the depot
|
||||
/// are reflected on the scenery (both views share the same Scenery/Trainset
|
||||
/// objects from <see cref="GameData"/>).
|
||||
/// </summary>
|
||||
public sealed class AppState
|
||||
{
|
||||
public static AppState Instance { get; } = new();
|
||||
|
||||
public Scenery? CurrentScenery { get; set; }
|
||||
public Trainset? CurrentTrainset { get; set; }
|
||||
}
|
||||
83
StarterNG/Classes/GameData.cs
Normal file
83
StarterNG/Classes/GameData.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace StarterNG.Classes;
|
||||
|
||||
public enum LoadPhase
|
||||
{
|
||||
Vehicles,
|
||||
Sceneries,
|
||||
Done
|
||||
}
|
||||
|
||||
/// <summary>A single progress update emitted while the game data loads.</summary>
|
||||
public readonly record struct LoadStatus(double Fraction, LoadPhase Phase, string? Detail);
|
||||
|
||||
/// <summary>
|
||||
/// Loads and holds the data shared across the launcher (vehicle database and
|
||||
/// parsed sceneries) so it is parsed once, at startup, behind the splash —
|
||||
/// instead of inside each view's constructor.
|
||||
/// </summary>
|
||||
public sealed class GameData
|
||||
{
|
||||
public static GameData Instance { get; } = new();
|
||||
|
||||
public VehicleDatabase Vehicles { get; } = new();
|
||||
public List<Scenery> Sceneries { get; } = new();
|
||||
|
||||
public bool Loaded { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses the vehicle database and all sceneries, reporting progress.
|
||||
/// Runs synchronously; call it from a background thread. Never throws.
|
||||
/// </summary>
|
||||
public void Load(IProgress<LoadStatus>? progress = null,
|
||||
string vehiclesDir = "databases/vehicles/",
|
||||
string sceneryDir = "scenery/")
|
||||
{
|
||||
if (Loaded)
|
||||
return;
|
||||
|
||||
var vehicleFiles = VehicleDatabase.EnumerateFiles(vehiclesDir);
|
||||
var scnFiles = EnumerateScenery(sceneryDir);
|
||||
int total = Math.Max(1, vehicleFiles.Count + scnFiles.Count);
|
||||
int done = 0;
|
||||
|
||||
// Phase 1 - vehicle database
|
||||
Vehicles.BeginLoad();
|
||||
foreach (string file in vehicleFiles)
|
||||
{
|
||||
progress?.Report(new LoadStatus((double)done / total, LoadPhase.Vehicles,
|
||||
Path.GetFileName(file)));
|
||||
try { Vehicles.LoadFile(file); }
|
||||
catch { /* skip malformed file */ }
|
||||
done++;
|
||||
}
|
||||
Vehicles.EndLoad();
|
||||
|
||||
// Phase 2 - sceneries
|
||||
foreach (string file in scnFiles)
|
||||
{
|
||||
progress?.Report(new LoadStatus((double)done / total, LoadPhase.Sceneries,
|
||||
Path.GetFileName(file)));
|
||||
try { Sceneries.Add(new Scenery(file)); }
|
||||
catch { /* skip unreadable scenery */ }
|
||||
done++;
|
||||
}
|
||||
|
||||
progress?.Report(new LoadStatus(1.0, LoadPhase.Done, null));
|
||||
Loaded = true;
|
||||
}
|
||||
|
||||
private static List<string> EnumerateScenery(string sceneryDir)
|
||||
{
|
||||
if (!Directory.Exists(sceneryDir))
|
||||
return new List<string>();
|
||||
|
||||
return Directory.GetFiles(sceneryDir, "*.scn")
|
||||
.Where(p => !Path.GetFileName(p).StartsWith("$"))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,11 @@ public class Scenery
|
||||
public List<Trainset> Trainsets;
|
||||
public string Group;
|
||||
public string Path;
|
||||
|
||||
// The file content with each trainset block replaced by a {{i}} placeholder,
|
||||
// used to rebuild the .scn on export.
|
||||
private readonly string _template;
|
||||
|
||||
public Scenery(string path)
|
||||
{
|
||||
this.Path = path;
|
||||
@@ -41,18 +46,23 @@ public class Scenery
|
||||
trainsetEntries.Add(match.Value);
|
||||
return $"{{{{{idx++}}}}}";
|
||||
});
|
||||
_template = content;
|
||||
|
||||
// 1:1 with placeholders - the Trainset ctor never throws (unparsable
|
||||
// blocks are kept verbatim), so indices stay aligned for export.
|
||||
foreach (string trainsetEntry in trainsetEntries)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trainsets.Add(new Trainset(trainsetEntry));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// skip malformed trainset entries so one bad scenery
|
||||
// doesn't break loading the rest
|
||||
}
|
||||
}
|
||||
Trainsets.Add(new Trainset(trainsetEntry));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the full .scn content with the (possibly modified) trainsets
|
||||
/// substituted back into their original positions.
|
||||
/// </summary>
|
||||
public string BuildExportContent()
|
||||
{
|
||||
string result = _template;
|
||||
for (int i = 0; i < Trainsets.Count; i++)
|
||||
result = result.Replace("{{" + i + "}}", Trainsets[i].ToSceneryEntry());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
51
StarterNG/Classes/Settings.cs
Normal file
51
StarterNG/Classes/Settings.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace StarterNG.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// Application settings backing the Settings view. Load/Save are stubs for now;
|
||||
/// the real persistence will be implemented later.
|
||||
/// </summary>
|
||||
public sealed class Settings
|
||||
{
|
||||
public static Settings Instance { get; } = new();
|
||||
|
||||
// General
|
||||
public string Language = "English";
|
||||
public bool Fullscreen;
|
||||
public bool PauseWhenInactive;
|
||||
public bool PauseOnStart;
|
||||
public int CursorSensitivity = 3;
|
||||
public bool InvertMouseHorizontal;
|
||||
public bool InvertMouseVertical;
|
||||
|
||||
// Other
|
||||
public string ExecutablePath = "eu07.exe"; // game executable used to launch
|
||||
public bool SelectExeAutomatically = true;
|
||||
public bool DebugMode;
|
||||
public bool VirtualShunting;
|
||||
|
||||
// Graphics (subset)
|
||||
public string Resolution = "1280x720";
|
||||
public bool VSync = true;
|
||||
public bool RenderShadows = true;
|
||||
|
||||
// Sound
|
||||
public bool SoundEnabled = true;
|
||||
public int Volume = 100;
|
||||
|
||||
// Starter
|
||||
public bool AutoCloseStarter;
|
||||
public bool LargeThumbnails;
|
||||
public bool AutoExpandSceneryTree;
|
||||
|
||||
/// <summary>Loads settings from disk. TODO: implement persistence.</summary>
|
||||
public void Load()
|
||||
{
|
||||
// intentionally empty for now
|
||||
}
|
||||
|
||||
/// <summary>Persists settings to disk. TODO: implement persistence.</summary>
|
||||
public void Save()
|
||||
{
|
||||
// intentionally empty for now
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,31 @@ public class Trainset
|
||||
public float Velocity;
|
||||
public string Description;
|
||||
public List<Dynamic> Vehicles;
|
||||
|
||||
/// <summary>Original .scn text of this trainset block (incl. endtrainset).</summary>
|
||||
public string RawEntry;
|
||||
|
||||
/// <summary>True if the block parsed cleanly; false blocks are exported verbatim.</summary>
|
||||
public bool Parsed;
|
||||
|
||||
public Trainset(string trainsetEntry)
|
||||
{
|
||||
|
||||
RawEntry = trainsetEntry;
|
||||
Vehicles = new List<Dynamic>();
|
||||
Description = "";
|
||||
try
|
||||
{
|
||||
ParseEntry(trainsetEntry);
|
||||
Parsed = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Parsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseEntry(string trainsetEntry)
|
||||
{
|
||||
List<string> tokens = Regex
|
||||
.Matches(trainsetEntry, @"/\*[\s\S]*?\*/|//[^\r\n]*|[^\s\r\n]+")
|
||||
.Select(m => m.Value)
|
||||
@@ -113,6 +133,13 @@ public class Trainset
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The .scn text for this trainset for export. Unparsed blocks are written
|
||||
/// back verbatim; parsed blocks are regenerated from the current vehicles.
|
||||
/// </summary>
|
||||
public string ToSceneryEntry() =>
|
||||
Parsed ? GetTrainsetEntry() + "endtrainset\n" : RawEntry;
|
||||
|
||||
public string GetTrainsetEntry()
|
||||
{
|
||||
string entry = "";
|
||||
@@ -120,8 +147,8 @@ public class Trainset
|
||||
entry += "trainset ";
|
||||
entry += this.Name + " ";
|
||||
entry += this.Track + " ";
|
||||
entry += this.Offset + " ";
|
||||
entry += this.Velocity + " ";
|
||||
entry += this.Offset.ToString(CultureInfo.InvariantCulture) + " ";
|
||||
entry += this.Velocity.ToString(CultureInfo.InvariantCulture) + " ";
|
||||
entry += "\n";
|
||||
foreach (Dynamic vehicle in Vehicles)
|
||||
{
|
||||
|
||||
@@ -94,6 +94,12 @@ public class VehicleDatabase
|
||||
/// <summary>Texture-uuid -> automatic-consist set that contains it.</summary>
|
||||
public Dictionary<string, VehicleSet> SetByTextureUuid { get; } = new();
|
||||
|
||||
/// <summary>Texture-uuid -> texture (includes wrecks, so set refs resolve).</summary>
|
||||
public Dictionary<string, VehicleTexture> TextureByUuid { get; } = new();
|
||||
|
||||
/// <summary>skinfile (without extension, lower-case) -> texture.</summary>
|
||||
public Dictionary<string, VehicleTexture> TextureBySkin { get; } = new();
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
@@ -102,32 +108,55 @@ public class VehicleDatabase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Loads the database. When a merged vehicles.json is present it is used,
|
||||
/// otherwise every per-vehicle *.json file in the directory is read.
|
||||
/// Loads the database in one call. When a merged vehicles.json is present
|
||||
/// it is used, otherwise every per-vehicle *.json file is read.
|
||||
/// Never throws: unreadable files are skipped.
|
||||
/// </summary>
|
||||
public void Load(string directory = "databases/vehicles/")
|
||||
{
|
||||
BeginLoad();
|
||||
foreach (string file in EnumerateFiles(directory))
|
||||
LoadFile(file);
|
||||
EndLoad();
|
||||
}
|
||||
|
||||
/// <summary>Resets the aggregate before an incremental load.</summary>
|
||||
public void BeginLoad()
|
||||
{
|
||||
Textures.Clear();
|
||||
GroupsById.Clear();
|
||||
Sets.Clear();
|
||||
SetByTextureUuid.Clear();
|
||||
TextureByUuid.Clear();
|
||||
TextureBySkin.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Finalises an incremental load (builds lookup indexes).</summary>
|
||||
public void EndLoad() => BuildSetIndex();
|
||||
|
||||
/// <summary>
|
||||
/// Files that make up the database: the merged vehicles.json if present,
|
||||
/// otherwise every per-vehicle *.json. Used to drive load progress.
|
||||
/// </summary>
|
||||
public static List<string> EnumerateFiles(string directory = "databases/vehicles/")
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
return new List<string>();
|
||||
|
||||
string merged = Path.Combine(directory, "vehicles.json");
|
||||
if (File.Exists(merged))
|
||||
{
|
||||
LoadMerged(merged);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string file in Directory.GetFiles(directory, "*.json"))
|
||||
LoadEntryFile(file);
|
||||
}
|
||||
return new List<string> { merged };
|
||||
|
||||
BuildSetIndex();
|
||||
return Directory.GetFiles(directory, "*.json").ToList();
|
||||
}
|
||||
|
||||
/// <summary>Ingests a single database file (merged or per-vehicle).</summary>
|
||||
public void LoadFile(string file)
|
||||
{
|
||||
if (string.Equals(Path.GetFileName(file), "vehicles.json", StringComparison.OrdinalIgnoreCase))
|
||||
LoadMerged(file);
|
||||
else
|
||||
LoadEntryFile(file);
|
||||
}
|
||||
|
||||
private void LoadMerged(string file)
|
||||
@@ -171,6 +200,12 @@ public class VehicleDatabase
|
||||
|
||||
foreach (var texture in entry.Textures)
|
||||
{
|
||||
// index every texture (wrecks too) so set / skin references resolve
|
||||
if (!string.IsNullOrEmpty(texture.Uuid))
|
||||
TextureByUuid[texture.Uuid!] = texture;
|
||||
if (!string.IsNullOrEmpty(texture.Skinfile))
|
||||
TextureBySkin.TryAdd(Path.GetFileNameWithoutExtension(texture.Skinfile).ToLowerInvariant(), texture);
|
||||
|
||||
// skip wrecks from the standard browser list
|
||||
if (texture.Wreck) continue;
|
||||
Textures.Add(texture);
|
||||
@@ -179,6 +214,25 @@ public class VehicleDatabase
|
||||
Sets.AddRange(entry.Sets);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the texture belongs to an automatic-consist set, returns all of that
|
||||
/// set's textures in their defined order; otherwise null.
|
||||
/// </summary>
|
||||
public List<VehicleTexture>? ResolveSet(VehicleTexture texture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(texture.Uuid)) return null;
|
||||
if (!SetByTextureUuid.TryGetValue(texture.Uuid!, out var set) || set.TextureRefs is null)
|
||||
return null;
|
||||
|
||||
var cars = new List<VehicleTexture>();
|
||||
foreach (string uuid in set.TextureRefs)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(uuid) && TextureByUuid.TryGetValue(uuid, out var tex))
|
||||
cars.Add(tex);
|
||||
}
|
||||
return cars.Count > 0 ? cars : null;
|
||||
}
|
||||
|
||||
private void BuildSetIndex()
|
||||
{
|
||||
foreach (var set in Sets)
|
||||
@@ -192,14 +246,36 @@ public class VehicleDatabase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Best miniature name for a texture (texture_mini -> mini_ref -> group mini).</summary>
|
||||
/// <summary>
|
||||
/// Miniature name for a texture: the texture_mini property, but if no .bmp
|
||||
/// for it exists, falls back to the mini of the group it belongs to.
|
||||
/// </summary>
|
||||
public string? ResolveMiniName(VehicleTexture texture)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(texture.TextureMini)) return texture.TextureMini;
|
||||
if (!string.IsNullOrEmpty(texture.MiniRef)) return texture.MiniRef;
|
||||
if (texture.Group is not null && GroupsById.TryGetValue(texture.Group, out var grp))
|
||||
if (!string.IsNullOrEmpty(texture.TextureMini) && MiniPath(texture.TextureMini) != null)
|
||||
return texture.TextureMini;
|
||||
|
||||
if (texture.Group != null && GroupsById.TryGetValue(texture.Group, out var grp)
|
||||
&& !string.IsNullOrEmpty(grp.Mini))
|
||||
return grp.Mini;
|
||||
return null;
|
||||
|
||||
return texture.TextureMini;
|
||||
}
|
||||
|
||||
/// <summary>Resolved mini for a skin file (matched case-insensitively), or null.</summary>
|
||||
public string? MiniForSkin(string? skinFile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skinFile)) return null;
|
||||
string key = Path.GetFileNameWithoutExtension(skinFile).ToLowerInvariant();
|
||||
return TextureBySkin.TryGetValue(key, out var tex) ? ResolveMiniName(tex) : null;
|
||||
}
|
||||
|
||||
/// <summary>The texture for a skin file (matched case-insensitively), or null.</summary>
|
||||
public VehicleTexture? TextureForSkin(string? skinFile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(skinFile)) return null;
|
||||
string key = Path.GetFileNameWithoutExtension(skinFile).ToLowerInvariant();
|
||||
return TextureBySkin.TryGetValue(key, out var tex) ? tex : null;
|
||||
}
|
||||
|
||||
/// <summary>Header label for the group a texture belongs to.</summary>
|
||||
@@ -213,12 +289,30 @@ public class VehicleDatabase
|
||||
return groupId ?? "";
|
||||
}
|
||||
|
||||
/// <summary>Resolves a miniature .bmp path under textures/mini/, or null if missing.</summary>
|
||||
// Case-insensitive index of mini .bmp files (built once).
|
||||
private static Dictionary<string, string>? _miniIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a miniature .bmp path under textures/mini/ case-insensitively,
|
||||
/// or null if missing. The directory is indexed once and reused.
|
||||
/// </summary>
|
||||
public static string? MiniPath(string? miniName, string miniDir = "textures/mini/")
|
||||
{
|
||||
if (string.IsNullOrEmpty(miniName)) return null;
|
||||
string path = Path.Combine(miniDir, miniName + ".bmp");
|
||||
return File.Exists(path) ? path : null;
|
||||
|
||||
var index = _miniIndex ??= BuildMiniIndex(miniDir);
|
||||
return index.TryGetValue(miniName!.ToLowerInvariant(), out var path) ? path : null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildMiniIndex(string miniDir)
|
||||
{
|
||||
var index = new Dictionary<string, string>();
|
||||
if (!Directory.Exists(miniDir))
|
||||
return index;
|
||||
|
||||
foreach (string file in Directory.GetFiles(miniDir, "*.bmp"))
|
||||
index[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = file;
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
StarterNG/SplashWindow.axaml
Normal file
25
StarterNG/SplashWindow.axaml
Normal file
@@ -0,0 +1,25 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:starterNg="clr-namespace:StarterNG"
|
||||
x:Class="StarterNG.SplashWindow"
|
||||
SystemDecorations="None"
|
||||
CanResize="False"
|
||||
ShowInTaskbar="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Width="520" Height="300"
|
||||
Background="#15171B">
|
||||
<Grid>
|
||||
<Image Source="/Assets/Logo/splash.png" Stretch="UniformToFill" />
|
||||
|
||||
<Border VerticalAlignment="Bottom" Background="#CC0E0F12" Padding="18,12">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="statusText"
|
||||
Foreground="#EEEEEE" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding [LoadingVehicles], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ProgressBar x:Name="progressBar"
|
||||
Minimum="0" Maximum="1" Value="0" Height="6" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
30
StarterNG/SplashWindow.axaml.cs
Normal file
30
StarterNG/SplashWindow.axaml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
using StarterNG.Classes;
|
||||
|
||||
namespace StarterNG;
|
||||
|
||||
public partial class SplashWindow : Window
|
||||
{
|
||||
public SplashWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>Updates the bar and status line. Must be called on the UI thread.</summary>
|
||||
public void Report(LoadStatus status)
|
||||
{
|
||||
progressBar.Value = status.Fraction;
|
||||
statusText.Text = Format(status);
|
||||
}
|
||||
|
||||
private static string Format(LoadStatus s)
|
||||
{
|
||||
string detail = string.IsNullOrEmpty(s.Detail) ? "" : $": {s.Detail}";
|
||||
return s.Phase switch
|
||||
{
|
||||
LoadPhase.Vehicles => App.Loc["LoadingVehicles"] + detail,
|
||||
LoadPhase.Sceneries => App.Loc["LoadingSceneries"] + detail,
|
||||
_ => App.Loc["LoadingDone"]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@
|
||||
<AvaloniaResource Include="Assets\Logo\maszyna_white.svg" />
|
||||
<None Remove="Assets\Logo\Other\Discord.svg" />
|
||||
<AvaloniaResource Include="Assets\Logo\Other\Discord.svg" />
|
||||
<None Remove="Assets\Logo\splash.png" />
|
||||
<AvaloniaResource Include="Assets\Logo\splash.png" />
|
||||
<None Remove="Assets\Test\ep07p.bmp" />
|
||||
<AvaloniaResource Include="Assets\Test\ep07p.bmp" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:sukiUi="clr-namespace:SukiUI.Controls;assembly=SukiUI"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:starterNg="clr-namespace:StarterNG"
|
||||
xmlns:views="clr-namespace:StarterNG.Views"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
|
||||
x:Class="StarterNG.Views.Depot">
|
||||
<Grid Margin="16" ColumnSpacing="16" RowSpacing="16">
|
||||
@@ -30,6 +31,8 @@
|
||||
<TextBox Name="searchBox" DockPanel.Dock="Top" Margin="0,0,0,8"
|
||||
Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
|
||||
TextChanged="SearchBox_OnTextChanged" />
|
||||
<!-- Folded groups (built in code-behind); collapsed groups hold no
|
||||
controls, so the list stays light and scrolls smoothly. -->
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Name="browserStack" Spacing="2" />
|
||||
</ScrollViewer>
|
||||
@@ -47,6 +50,7 @@
|
||||
<ComboBox Name="sceneryConsistCombo" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Stretch" MaxDropDownHeight="420"
|
||||
PlaceholderText="{Binding [LoadFromScenery], Source={x:Static starterNg:App.Loc}}"
|
||||
DropDownOpened="SceneryConsistCombo_OnDropDownOpened"
|
||||
SelectionChanged="SceneryConsistCombo_OnSelectionChanged" />
|
||||
</Grid>
|
||||
</sukiUi:GroupBox>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@
|
||||
|
||||
<Button Grid.Column="0" Height="36" Classes="Flat"
|
||||
VerticalAlignment="Bottom" Grid.Row="3" Grid.ColumnSpan="2"
|
||||
Click="StartButton_OnClick"
|
||||
Content="{Binding [Start], Source={x:Static starterNg:App.Loc}}">
|
||||
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media.Imaging;
|
||||
using StarterNG.Classes;
|
||||
@@ -16,19 +19,9 @@ public partial class Scenarios : UserControl
|
||||
public Scenarios()
|
||||
{
|
||||
InitializeComponent();
|
||||
sceneries = new List<Scenery>();
|
||||
|
||||
// load sceneries
|
||||
List<string> scnFiles = Directory.GetFiles("scenery/", "*.scn").ToList();
|
||||
foreach (string scnFile in scnFiles)
|
||||
{
|
||||
// skip temp sceneries
|
||||
if (Path.GetFileName(scnFile).StartsWith("$"))
|
||||
continue;
|
||||
|
||||
// Parse all files
|
||||
sceneries.Add(new Scenery(scnFile));
|
||||
}
|
||||
// Sceneries are parsed once at startup (behind the splash).
|
||||
sceneries = GameData.Instance.Sceneries;
|
||||
|
||||
var groupNodes = new Dictionary<string, TreeViewItem>();
|
||||
|
||||
@@ -109,15 +102,18 @@ public partial class Scenarios : UserControl
|
||||
if (vItem?.Tag is not int vTag)
|
||||
return;
|
||||
Trainset selectedTrainset = selectedScn.Trainsets[vTag];
|
||||
|
||||
// share the selection so the depot edits this consist in place
|
||||
AppState.Instance.CurrentScenery = selectedScn;
|
||||
AppState.Instance.CurrentTrainset = selectedTrainset;
|
||||
|
||||
var db = GameData.Instance.Vehicles;
|
||||
foreach (var train in selectedTrainset.Vehicles)
|
||||
{
|
||||
string path = Path.Combine(
|
||||
"textures",
|
||||
"mini",
|
||||
train.SkinFile + ".bmp"
|
||||
);
|
||||
|
||||
if (!File.Exists(path))
|
||||
// thumbnail name comes from the matching texture's texture_mini
|
||||
string miniName = db.MiniForSkin(train.SkinFile) ?? train.SkinFile;
|
||||
string? path = VehicleDatabase.MiniPath(miniName);
|
||||
if (path is null)
|
||||
{
|
||||
// fallback
|
||||
continue;
|
||||
@@ -135,4 +131,46 @@ public partial class Scenarios : UserControl
|
||||
}
|
||||
missionDescription.Text = selectedTrainset.Description;
|
||||
}
|
||||
|
||||
// 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/$<name>.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;
|
||||
|
||||
// launch the game: -s $<name>.scn -v <vehicle>
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(StarterNG.Classes.Settings.Instance.ExecutablePath)
|
||||
{
|
||||
Arguments = $"-s {exportName} -v {vehicle}",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// executable not found / not configured yet
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user