mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-05 21:29:09 +01:00
Strip all comments from the C# sources
Removes every // and /// comment from the 28 .cs files, -1331 lines.
Done with a lexer rather than a regex: the sources carry plenty of // inside
string literals (@"//\$decor\b", the //$o trainset directives, sb.Append("// "),
"https://wiki.eu07.pl/") and those must survive. It tracks regular, verbatim and
interpolated strings, interpolation holes and char literals. Every file was
checked by comparing the sequence of string-literal characters before and after
the strip; all 28 match. BOMs preserved, blank-line runs collapsed to one.
Claude-Session: https://claude.ai/code/session_01HamVNWE9mwmz1vs2iEpNQq
This commit is contained in:
@@ -11,18 +11,9 @@ using StarterNG.Classes;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Right-click menu shared by the consist lists (the "Vehicles" list in Scenarios
|
||||
/// and the "Map consists" list in the depot). Offers: save to warehouse, load from
|
||||
/// warehouse (each entry deletable via an "✕"), copy/paste via the clipboard, and -
|
||||
/// when <c>removeAll</c> is supplied - clearing the consist (Shift+Delete).
|
||||
/// </summary>
|
||||
public static class ConsistContextMenu
|
||||
{
|
||||
/// <param name="owner">Control the menu/flyouts anchor to (for clipboard + popups).</param>
|
||||
/// <param name="getTarget">Supplies the trainset to act on when the menu opens.</param>
|
||||
/// <param name="applyVehicles">Replaces the target's vehicles and refreshes the view.</param>
|
||||
/// <param name="removeAll">When non-null, adds a "remove all vehicles" entry.</param>
|
||||
|
||||
public static ContextMenu Build(
|
||||
Control owner,
|
||||
Func<Trainset?> getTarget,
|
||||
@@ -30,7 +21,7 @@ public static class ConsistContextMenu
|
||||
Action? removeAll = null)
|
||||
{
|
||||
var menu = new ContextMenu();
|
||||
// Rebuild the entries each time it opens so the warehouse list stays fresh.
|
||||
|
||||
menu.Opening += (_, _) => Populate(menu, owner, getTarget, applyVehicles, removeAll);
|
||||
return menu;
|
||||
}
|
||||
@@ -42,12 +33,10 @@ public static class ConsistContextMenu
|
||||
var target = getTarget();
|
||||
bool hasVehicles = target is { Vehicles.Count: > 0 };
|
||||
|
||||
// ── Save current consist to the warehouse ──────────────────────────────
|
||||
var save = new MenuItem { Header = App.Loc["PresetSave"], IsEnabled = hasVehicles };
|
||||
save.Click += (_, _) => ShowSaveFlyout(owner, target);
|
||||
menu.Items.Add(save);
|
||||
|
||||
// ── Load from the warehouse (submenu; each entry deletable) ────────────
|
||||
var load = new MenuItem { Header = App.Loc["PresetLoad"] };
|
||||
var presets = PresetStore.All();
|
||||
if (presets.Count == 0)
|
||||
@@ -82,7 +71,6 @@ public static class ConsistContextMenu
|
||||
|
||||
menu.Items.Add(new Separator());
|
||||
|
||||
// ── Clipboard (full trainset text) ─────────────────────────────────────
|
||||
var copy = new MenuItem { Header = App.Loc["ConsistCopy"], IsEnabled = hasVehicles };
|
||||
copy.Click += async (_, _) =>
|
||||
{
|
||||
@@ -103,7 +91,6 @@ public static class ConsistContextMenu
|
||||
};
|
||||
menu.Items.Add(paste);
|
||||
|
||||
// ── Remove all vehicles (depot only) ───────────────────────────────────
|
||||
if (removeAll != null)
|
||||
{
|
||||
menu.Items.Add(new Separator());
|
||||
@@ -118,8 +105,6 @@ public static class ConsistContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
// A warehouse entry: the name loads the preset on click, the trailing "✕"
|
||||
// deletes it from the warehouse.
|
||||
private static MenuItem BuildPresetItem(ContextMenu menu, TrainsetPreset preset,
|
||||
Action<List<Dynamic>> applyVehicles)
|
||||
{
|
||||
@@ -134,9 +119,9 @@ public static class ConsistContextMenu
|
||||
ToolTip.SetTip(del, App.Loc["PresetDelete"]);
|
||||
del.Click += (_, e) =>
|
||||
{
|
||||
e.Handled = true; // don't let the click also load the preset
|
||||
e.Handled = true;
|
||||
PresetStore.Delete(preset.Name);
|
||||
menu.Close(); // reopen to see the updated warehouse
|
||||
menu.Close();
|
||||
};
|
||||
|
||||
var label = new TextBlock { Text = preset.Name, VerticalAlignment = VerticalAlignment.Center };
|
||||
@@ -156,7 +141,6 @@ public static class ConsistContextMenu
|
||||
return item;
|
||||
}
|
||||
|
||||
// Small popup to name the preset before saving it to the warehouse.
|
||||
private static void ShowSaveFlyout(Control owner, Trainset? target)
|
||||
{
|
||||
if (target is null || target.Vehicles.Count == 0)
|
||||
|
||||
@@ -21,25 +21,19 @@ namespace StarterNG.Views;
|
||||
|
||||
public partial class Depot : UserControl
|
||||
{
|
||||
// Data is parsed once at startup (behind the splash) and shared here.
|
||||
|
||||
private readonly VehicleDatabase _db = GameData.Instance.Vehicles;
|
||||
private readonly List<Scenery> _sceneries = GameData.Instance.Sceneries;
|
||||
|
||||
// The consist being edited: a list of units (each unit = one or more cars).
|
||||
private readonly List<ConsistItem> _consist = new();
|
||||
private ConsistItem? _selected;
|
||||
|
||||
// The scenery trainset this consist is bound to; edits are written back to it
|
||||
// so the scenery is modified. Null when building an unattached consist.
|
||||
private Trainset? _editingTrainset;
|
||||
|
||||
// Shared, size-limited thumbnail cache (decoded down to display height).
|
||||
private readonly Dictionary<string, Bitmap?> _miniCache = new();
|
||||
private int ConsistThumbHeight =>
|
||||
StarterNG.Classes.Settings.Instance.LargeThumbnails ? 68 : 34;
|
||||
|
||||
// Display height of the mini under the vehicle list; the class combo uses
|
||||
// the same size.
|
||||
private const int MiniPreviewHeight = 54;
|
||||
|
||||
private const int CompactThumbHeight = 24;
|
||||
@@ -47,35 +41,25 @@ public partial class Depot : UserControl
|
||||
private readonly Cursor _hand = new(StandardCursorType.Hand);
|
||||
private int _nameCounter;
|
||||
|
||||
// shared RNG for the "random load / random amount" consist tools
|
||||
private static readonly Random _rng = new();
|
||||
|
||||
// suppresses combo SelectionChanged handlers while we populate them
|
||||
private bool _suppress;
|
||||
private bool _syncingCombos; // set while the combos mirror a pick
|
||||
private bool _syncingCombos;
|
||||
|
||||
// debounces search input so we don't rebuild on every keystroke
|
||||
private DispatcherTimer? _searchTimer;
|
||||
|
||||
// the scenery whose consists are listed on the right (from the Scenarios tab)
|
||||
private Scenery? _listScenery;
|
||||
|
||||
// active database filters
|
||||
private Func<string?, bool>? _categoryFilter; // matches a category letter, null = none chosen
|
||||
private string? _classFilter; // group mini (e.g. EU07), null = all in category
|
||||
private Func<string?, bool>? _categoryFilter;
|
||||
private string? _classFilter;
|
||||
|
||||
// the vehicle currently highlighted in the flat list (drives the mini preview
|
||||
// and the single Add button)
|
||||
private VehicleTexture? _browserSelected;
|
||||
|
||||
// drag-drop payload while a pointer drag is in progress
|
||||
private VehicleTexture? _dragTexture;
|
||||
private int _dragConsistIndex = -1;
|
||||
private const string DragVehicleFormat = "starterng/vehicle";
|
||||
private const string DragConsistFormat = "starterng/consist-index";
|
||||
|
||||
// Armed on press, started only after the pointer moves past DragThreshold.
|
||||
// Starting DoDragDropAsync on every click freezes Avalonia (pointer capture).
|
||||
private PointerPressedEventArgs? _pendingDragPress;
|
||||
private Point _pendingDragOrigin;
|
||||
private VehicleTexture? _pendingDragTexture;
|
||||
@@ -84,17 +68,13 @@ public partial class Depot : UserControl
|
||||
private bool _dragSessionActive;
|
||||
private const double DragThreshold = 6;
|
||||
|
||||
// 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("#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
|
||||
// vehicles use lowercase letters; wagons use uppercase letters.
|
||||
private static readonly (string LocKey, Func<string?, bool> Match)[] CategoryDefs =
|
||||
{
|
||||
("CatElectricLoco", c => c == "e"),
|
||||
@@ -136,22 +116,18 @@ public partial class Depot : UserControl
|
||||
("CatWagonsX", c => c == "X"),
|
||||
("CatWagonsY", c => c == "Y"),
|
||||
("CatWagonsZ", c => c == "Z"),
|
||||
|
||||
|
||||
("CatOther", IsOtherCat),
|
||||
};
|
||||
|
||||
// lowercase letters that already have a named type (so they aren't "other")
|
||||
private static readonly HashSet<string> NamedLowerCats =
|
||||
new(StringComparer.Ordinal) { "a", "b", "c", "d", "e", "f", "h", "n", "o", "p", "r", "s", "t", "x", "z" };
|
||||
|
||||
private static bool IsOtherCat(string? c) =>
|
||||
c is { Length: 1 } && char.IsLower(c[0]) && !NamedLowerCats.Contains(c);
|
||||
|
||||
// Car-type suffixes (longest first) used only for the consist unit label.
|
||||
private static readonly string[] CarSuffixes = { "sa", "sb", "ra", "rb", "s" };
|
||||
|
||||
// Coupling-bit localisation keys in mask-bit order (bit i = value 1<<i), per
|
||||
// the wiki: https://wiki.eu07.pl/index.php?title=Wpisy_hamulca_dla_pojazdow
|
||||
private static readonly string[] CouplingBitKeys =
|
||||
{ "CplMechanical", "CplBrake", "CplControl", "CplHighVoltage",
|
||||
"CplGangway", "CplAuxPneumatic", "CplHeating", "CplWorkshop" };
|
||||
@@ -170,17 +146,15 @@ public partial class Depot : UserControl
|
||||
BuildList();
|
||||
};
|
||||
|
||||
// Defer population so the tab renders immediately, then fills in.
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
hideArchivalCheck.IsChecked = StarterNG.Classes.Settings.Instance.HideArchivalVehicles;
|
||||
InitClassComboTemplates();
|
||||
PopulateCategoryCombo(); // -> populates class combo -> builds browser
|
||||
PopulateSceneryConsists(); // -> lists the current scenery's consists
|
||||
PopulateCategoryCombo();
|
||||
PopulateSceneryConsists();
|
||||
RebuildConsist();
|
||||
}, DispatcherPriority.Background);
|
||||
|
||||
// Consist strip accepts drops from the browser and reorders via card drag.
|
||||
DragDrop.SetAllowDrop(consistDropTarget, true);
|
||||
consistDropTarget.AddHandler(DragDrop.DragOverEvent, Consist_OnDragOver);
|
||||
consistDropTarget.AddHandler(DragDrop.DropEvent, Consist_OnDrop);
|
||||
@@ -190,9 +164,6 @@ public partial class Depot : UserControl
|
||||
miniPreviewPanel.AddHandler(InputElement.PointerMovedEvent, PendingDrag_OnPointerMoved, handledEventsToo: true);
|
||||
miniPreviewPanel.AddHandler(InputElement.PointerReleasedEvent, PendingDrag_OnPointerReleased, handledEventsToo: true);
|
||||
|
||||
// Switching tabs only toggles IsVisible (the view stays in the visual
|
||||
// tree), so refresh whenever the depot becomes visible - this picks up a
|
||||
// scenery changed in the Scenarios tab in the meantime.
|
||||
AttachedToVisualTree += (_, _) => SyncFromSelection();
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
@@ -203,7 +174,7 @@ public partial class Depot : UserControl
|
||||
|
||||
private void SyncFromSelection()
|
||||
{
|
||||
// pick up Settings-tab changes to the archival-vehicle filter
|
||||
|
||||
bool hideArchival = StarterNG.Classes.Settings.Instance.HideArchivalVehicles;
|
||||
if (hideArchivalCheck.IsChecked != hideArchival)
|
||||
{
|
||||
@@ -213,19 +184,15 @@ public partial class Depot : UserControl
|
||||
BuildList();
|
||||
}
|
||||
|
||||
// refresh the on-map consists list to the scenery picked in the Scenarios tab
|
||||
PopulateSceneryConsists();
|
||||
|
||||
var trainset = AppState.Instance.CurrentTrainset;
|
||||
if (trainset != null && !ReferenceEquals(trainset, _editingTrainset))
|
||||
LoadTrainset(trainset);
|
||||
else if (_editingTrainset != null)
|
||||
RebuildConsist(); // pick up LargeThumbnails / filter-driven list changes
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Loads a scenery trainset into the editor and binds edits back to it.
|
||||
// Consecutive cars that belong to the same auto-consist set are regrouped
|
||||
// into one locked unit so a scenery EN57 shows as a single vehicle.
|
||||
private void LoadTrainset(Trainset trainset)
|
||||
{
|
||||
_editingTrainset = trainset;
|
||||
@@ -234,7 +201,7 @@ public partial class Depot : UserControl
|
||||
var cars = trainset.Vehicles.Select(v =>
|
||||
{
|
||||
var c = v.Clone();
|
||||
c.MiniName = _db.MiniForSkin(c.SkinFile) ?? c.MiniName; // mini from texture_mini
|
||||
c.MiniName = _db.MiniForSkin(c.SkinFile) ?? c.MiniName;
|
||||
return c;
|
||||
}).ToList();
|
||||
|
||||
@@ -267,8 +234,7 @@ public partial class Depot : UserControl
|
||||
{
|
||||
Cars = group,
|
||||
Grouped = true,
|
||||
// FirstOrDefault(predicate) defaults the enum to Headdriver(0) when
|
||||
// every car is Nobody — that re-staffed decorative consists (#12).
|
||||
|
||||
Driver = UnitDriver(group)
|
||||
});
|
||||
i = j;
|
||||
@@ -276,7 +242,6 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: group consecutive cars sharing a unit number (e.g. EN57-001ra/s/rb)
|
||||
if (IsUnitCar(cars[i]))
|
||||
{
|
||||
string key = UnitKey(cars[i]);
|
||||
@@ -312,7 +277,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// First staffed occupancy on the unit, or Nobody when the whole unit is unstaffed.
|
||||
private static eDriverType UnitDriver(IReadOnlyList<Dynamic> group) =>
|
||||
group.Select(c => c.DriverType)
|
||||
.Where(d => d != eDriverType.Nobody)
|
||||
@@ -325,10 +289,6 @@ public partial class Depot : UserControl
|
||||
AppState.Instance.StartingVehicleName = _selected.Cars[0].Name;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------- series / unit helpers
|
||||
|
||||
// Removes a trailing car-type suffix that directly follows a digit
|
||||
// ("EN57-001ra" -> "EN57-001", "EN57ra" -> "EN57", "EP07-332" -> unchanged).
|
||||
private static string StripCarSuffix(string name)
|
||||
{
|
||||
foreach (string suf in CarSuffixes)
|
||||
@@ -345,23 +305,15 @@ public partial class Depot : UserControl
|
||||
|
||||
private static string UnitKey(Dynamic d) => StripCarSuffix(Base(d.SkinFile));
|
||||
|
||||
// True when the skin has a car-type suffix (so it is part of a multi-car unit).
|
||||
private static bool IsUnitCar(Dynamic d) => UnitKey(d) != Base(d.SkinFile);
|
||||
|
||||
// Vehicle class = the "mini" of the group the texture belongs to (e.g. EU07).
|
||||
// Resolved once at load (see VehicleDatabase.BuildTextureIndex); read the cache
|
||||
// here so search/filter never repeats the group lookup per keystroke.
|
||||
private static string ClassOf(VehicleTexture t) => t.ResolvedClass;
|
||||
|
||||
// Vehicle type = the group's "category" letter (also resolved once at load).
|
||||
private static string? CategoryOf(VehicleTexture t) => t.ResolvedCategory;
|
||||
|
||||
// ------------------------------------------------------------ category combo
|
||||
|
||||
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();
|
||||
foreach (var (locKey, match) in CategoryDefs)
|
||||
@@ -384,9 +336,6 @@ public partial class Depot : UserControl
|
||||
BuildList();
|
||||
}
|
||||
|
||||
// The dropdown shows the big stacked miniature, the closed combo a one-line
|
||||
// row. Plain "=": SelectionBoxItemTemplate coerces to ItemTemplate while
|
||||
// unset, so "??=" would never assign.
|
||||
private void InitClassComboTemplates()
|
||||
{
|
||||
classCombo.ItemTemplate = new FuncDataTemplate<string>(
|
||||
@@ -395,9 +344,6 @@ public partial class Depot : UserControl
|
||||
(cls, _) => ClassComboContent(cls, large: false), false);
|
||||
}
|
||||
|
||||
// Fills the class combo for the current category filter; the caller owns the
|
||||
// selection and _classFilter. Runs under _suppress.
|
||||
// No "All" entry: an unselected class means "every class in the category".
|
||||
private void FillClassCombo()
|
||||
{
|
||||
classCombo.Items.Clear();
|
||||
@@ -405,8 +351,6 @@ public partial class Depot : UserControl
|
||||
classCombo.Items.Add(cls);
|
||||
}
|
||||
|
||||
// Points both combos at the given category/class pair without firing their
|
||||
// handlers. Returns false when they already matched.
|
||||
private bool ApplyFilters(string? category, string? cls)
|
||||
{
|
||||
var catItem = categoryCombo.Items.OfType<ComboBoxItem>()
|
||||
@@ -433,7 +377,6 @@ public partial class Depot : UserControl
|
||||
return true;
|
||||
}
|
||||
|
||||
// Every texture of a class shares its category.
|
||||
private string? CategoryOfClass(string cls) =>
|
||||
_db.Textures.FirstOrDefault(t => string.Equals(ClassOf(t), cls, StringComparison.OrdinalIgnoreCase))
|
||||
is { } t ? CategoryOf(t) : null;
|
||||
@@ -491,7 +434,6 @@ public partial class Depot : UserControl
|
||||
string? cls = classCombo.SelectedItem as string;
|
||||
_classFilter = cls;
|
||||
|
||||
// ApplyFilters refills this very combo, so it cannot run from here.
|
||||
if (cls != null && _categoryFilter == null &&
|
||||
PostSyncing(() => { ApplyFilters(CategoryOfClass(cls), cls); BuildList(); }))
|
||||
return;
|
||||
@@ -499,9 +441,6 @@ public partial class Depot : UserControl
|
||||
BuildList();
|
||||
}
|
||||
|
||||
// Hovering over the Type/Class combos and scrolling cycles the selection
|
||||
// without having to open the dropdown (scroll up = previous, down = next).
|
||||
// When the dropdown is open the wheel scrolls the list as usual.
|
||||
private void Combo_OnPointerWheel(object? sender, PointerWheelEventArgs e)
|
||||
{
|
||||
if (sender is not ComboBox combo || combo.IsDropDownOpen)
|
||||
@@ -512,9 +451,9 @@ public partial class Depot : UserControl
|
||||
return;
|
||||
|
||||
int index = combo.SelectedIndex;
|
||||
if (e.Delta.Y > 0) // scroll up -> previous
|
||||
if (e.Delta.Y > 0)
|
||||
index = index <= 0 ? 0 : index - 1;
|
||||
else if (e.Delta.Y < 0) // scroll down -> next
|
||||
else if (e.Delta.Y < 0)
|
||||
index = index < 0 ? 0 : Math.Min(index + 1, count - 1);
|
||||
else
|
||||
return;
|
||||
@@ -523,15 +462,9 @@ public partial class Depot : UserControl
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- vehicle browser
|
||||
|
||||
// Flat list of plain-text vehicle entries (no expanders, like the original
|
||||
// Starter). Empty by default: an entry only appears once a category or class
|
||||
// is chosen or a search is typed. Picking one drives the mini preview and the
|
||||
// Add button.
|
||||
private void BuildList()
|
||||
{
|
||||
// reset selection-dependent UI
|
||||
|
||||
_browserSelected = null;
|
||||
miniPreview.Source = null;
|
||||
addVehicleButton.IsEnabled = false;
|
||||
@@ -542,8 +475,6 @@ public partial class Depot : UserControl
|
||||
string search = searchBox.Text?.Trim() ?? "";
|
||||
bool hasSearch = search.Length > 0;
|
||||
|
||||
// no filter and nothing searched -> keep the list empty (a class counts
|
||||
// as a filter: it can be picked without a category)
|
||||
if (_categoryFilter == null && _classFilter == null && !hasSearch)
|
||||
return;
|
||||
|
||||
@@ -572,7 +503,6 @@ public partial class Depot : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
// 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 })
|
||||
@@ -630,7 +560,7 @@ public partial class Depot : UserControl
|
||||
private void ShowTextureInfo(VehicleTexture texture)
|
||||
{
|
||||
textureInfoPanel.Children.Clear();
|
||||
// The rows are trimmed, so the tooltip carries the full text.
|
||||
|
||||
var full = new List<string>();
|
||||
void Row(string label, string? value)
|
||||
{
|
||||
@@ -659,29 +589,24 @@ public partial class Depot : UserControl
|
||||
ToolTip.SetTip(textureInfoPanel, string.Join("\n", full));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Pascal actAddVehiclesCategory — every skin currently listed in the browser.
|
||||
private void AddAllCategoryButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
foreach (var t in CurrentBrowserTextures())
|
||||
InsertTextureAt(t, _consist.Count);
|
||||
}
|
||||
|
||||
// Pascal actAddVehiclesMMD — first skin per distinct model/MMD.
|
||||
private void AddUniqueMmdButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -713,7 +638,6 @@ public partial class Depot : UserControl
|
||||
!string.Equals(ClassOf(t), _classFilter, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Hide subsequent multi-unit cars; adding the lead expands the whole set.
|
||||
if (_db.IsSetFollower(t))
|
||||
return false;
|
||||
|
||||
@@ -734,8 +658,6 @@ public partial class Depot : UserControl
|
||||
|| C(t.Meta?.Vehicle) || C(t.Meta?.Operator) || C(ClassOf(t));
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (_selected is null)
|
||||
@@ -767,8 +689,7 @@ public partial class Depot : UserControl
|
||||
|
||||
private string BrowserLabel(VehicleTexture texture, IReadOnlyList<VehicleTexture>? set)
|
||||
{
|
||||
// When texture_mini is missing or only the class/category mini, use the
|
||||
// skin basename so the list doesn't show the category name as the vehicle.
|
||||
|
||||
string name;
|
||||
if (!string.IsNullOrEmpty(texture.TextureMini) &&
|
||||
!string.Equals(texture.TextureMini, texture.ResolvedClass, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -783,8 +704,6 @@ public partial class Depot : UserControl
|
||||
return name;
|
||||
}
|
||||
|
||||
// Clicking "Add" inserts after the selected vehicle (or appends). If the
|
||||
// texture belongs to an auto-consist set, the whole set is one locked unit.
|
||||
private void AddTexture(VehicleTexture texture)
|
||||
{
|
||||
int at = _consist.Count;
|
||||
@@ -817,8 +736,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Like the Pascal MatchOccupancy: only powered units get a driver, and only
|
||||
// when the consist has no crew yet (or the unit is placed at the front).
|
||||
private void MatchOccupancy(ConsistItem item, int position)
|
||||
{
|
||||
item.Driver = eDriverType.Nobody;
|
||||
@@ -839,20 +756,14 @@ public partial class Depot : UserControl
|
||||
private static bool IsPoweredCategory(string? c) =>
|
||||
c is "e" or "s" or "p" or "z" or "a";
|
||||
|
||||
// Every caption here is read from App.Loc when it is built, so a language
|
||||
// switch has to build them again.
|
||||
public void RebuildAfterLanguageChange()
|
||||
{
|
||||
PopulateCategoryCombo(); // -> class combo -> browser list
|
||||
PopulateCategoryCombo();
|
||||
PopulateSceneryConsists();
|
||||
RebuildConsist(); // -> train stats
|
||||
RebuildConsist();
|
||||
UpdateDetails();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- scenery combos
|
||||
|
||||
// Lists the consists of the scenery currently selected in the Scenarios tab,
|
||||
// so they can be picked and edited here.
|
||||
private void PopulateSceneryConsists()
|
||||
{
|
||||
_suppress = true;
|
||||
@@ -878,7 +789,7 @@ public partial class Depot : UserControl
|
||||
Content = ConsistListContent(trainset),
|
||||
Tag = trainset
|
||||
};
|
||||
// right-click: warehouse save/load, clipboard copy/paste, clear-all
|
||||
|
||||
var ts = trainset;
|
||||
entry.ContextMenu = ConsistContextMenu.Build(
|
||||
entry,
|
||||
@@ -894,7 +805,6 @@ public partial class Depot : UserControl
|
||||
_suppress = false;
|
||||
}
|
||||
|
||||
// Selecting a consist on the map loads it into the editor below.
|
||||
private void SceneryConsistList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
@@ -911,8 +821,6 @@ public partial class Depot : UserControl
|
||||
!string.IsNullOrEmpty(trainset.Description) &&
|
||||
trainset.Description.TrimStart().StartsWith("-", StringComparison.Ordinal);
|
||||
|
||||
// Shift+Delete clears the selected scenery consist (same as the context-menu
|
||||
// "remove all vehicles" entry).
|
||||
private void SceneryConsistList_OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Delete && e.KeyModifiers.HasFlag(KeyModifiers.Shift) &&
|
||||
@@ -924,9 +832,6 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces a scenery consist's vehicles (from a warehouse preset or a pasted
|
||||
// clipboard consist), keeping the consist's map slot (name/track/offset), then
|
||||
// loads it into the editor and refreshes the list.
|
||||
private void ApplyConsist(Trainset target, List<Dynamic> vehicles)
|
||||
{
|
||||
target.Vehicles = vehicles;
|
||||
@@ -936,7 +841,6 @@ public partial class Depot : UserControl
|
||||
PopulateSceneryConsists();
|
||||
}
|
||||
|
||||
// Clears every vehicle from a scenery consist.
|
||||
private void RemoveAllVehicles(Trainset target)
|
||||
{
|
||||
target.Vehicles = new List<Dynamic>();
|
||||
@@ -946,7 +850,6 @@ public partial class Depot : UserControl
|
||||
PopulateSceneryConsists();
|
||||
}
|
||||
|
||||
// List caption for a scenery consist — grows with the list width (ellipsis).
|
||||
private static Control ConsistListContent(Trainset trainset) =>
|
||||
new TextBlock
|
||||
{
|
||||
@@ -972,8 +875,6 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ consist edits
|
||||
|
||||
private Dynamic MakeDynamic(VehicleTexture texture)
|
||||
{
|
||||
string skin = Base(texture.Skinfile);
|
||||
@@ -987,8 +888,8 @@ public partial class Depot : UserControl
|
||||
MmdFile = string.IsNullOrEmpty(texture.Model) ? skin : texture.Model!,
|
||||
Offset = 0f,
|
||||
DriverType = eDriverType.Nobody,
|
||||
Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe }, // 3
|
||||
HasVelocity = true, // emit a canonical "0" velocity in the trainset entry
|
||||
Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe },
|
||||
HasVelocity = true,
|
||||
MiniName = _db.ResolveMiniName(texture)
|
||||
};
|
||||
}
|
||||
@@ -1079,10 +980,9 @@ public partial class Depot : UserControl
|
||||
if (ReferenceEquals(_selected, item))
|
||||
SyncStartingVehicle();
|
||||
RebuildConsist();
|
||||
AppState.Instance.NotifyChanged(); // crew change may enable/disable Start
|
||||
AppState.Instance.NotifyChanged();
|
||||
}
|
||||
|
||||
// Breaks a grouped unit into individual single-car items.
|
||||
private void SplitItem(ConsistItem item)
|
||||
{
|
||||
int i = _consist.IndexOf(item);
|
||||
@@ -1103,8 +1003,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- consist render
|
||||
|
||||
private void RebuildConsist()
|
||||
{
|
||||
consistStack.Children.Clear();
|
||||
@@ -1112,8 +1010,7 @@ public partial class Depot : UserControl
|
||||
{
|
||||
var item = _consist[i];
|
||||
consistStack.Children.Add(BuildCard(item, i));
|
||||
// Coupler between units, plus a trailing coupler after the last unit
|
||||
// so end-of-train coupling / lights can be set (#10).
|
||||
|
||||
if (i < _consist.Count - 1)
|
||||
consistStack.Children.Add(BuildCoupler(item));
|
||||
else
|
||||
@@ -1126,9 +1023,6 @@ public partial class Depot : UserControl
|
||||
WriteBackToScenery();
|
||||
}
|
||||
|
||||
// Flattens the edited consist back onto the bound scenery trainset so the
|
||||
// change is reflected on the scenery. Driver goes on each unit's lead car;
|
||||
// a flipped unit writes its cars in reverse order.
|
||||
private void WriteBackToScenery()
|
||||
{
|
||||
if (_editingTrainset is null)
|
||||
@@ -1148,9 +1042,6 @@ public partial class Depot : UserControl
|
||||
RefreshConsistListLabels();
|
||||
}
|
||||
|
||||
// Resolves the .fiz physics for a consist car: the .fiz is named after the
|
||||
// model, so try the database model first, then the scenery mmd token, then the
|
||||
// skin file name.
|
||||
private Physics? PhysicsFor(Dynamic car)
|
||||
{
|
||||
string? dbModel = _db.TextureForSkin(car.SkinFile)?.Model;
|
||||
@@ -1159,10 +1050,6 @@ public partial class Depot : UserControl
|
||||
?? Physics.For(car.DataFolder, car.SkinFile);
|
||||
}
|
||||
|
||||
// Recomputes every coupling from the vehicles' .fiz AllowedFlag, like the
|
||||
// original Starter's AutoCoupler: the coupling is the bits common to the
|
||||
// facing ends, minus the multiple-unit (control) bit when the control types
|
||||
// differ. Called when a vehicle is added or replaced.
|
||||
private void AutoConnectAll()
|
||||
{
|
||||
var flat = new List<(Dynamic car, bool flipped)>();
|
||||
@@ -1181,10 +1068,9 @@ public partial class Depot : UserControl
|
||||
var lp = PhysicsFor(left);
|
||||
var rp = PhysicsFor(right);
|
||||
|
||||
// GetMaxCoupler: the end facing the neighbour (swapped when flipped)
|
||||
int leftMax = lp == null ? 3 : (lf ? lp.AllowedFlagA : lp.AllowedFlagB);
|
||||
int rightMax = rp == null ? 3 : (rf ? rp.AllowedFlagB : rp.AllowedFlagA);
|
||||
int common = leftMax & rightMax; // CommonCoupler = shared flag bits
|
||||
int common = leftMax & rightMax;
|
||||
|
||||
string lct = lp == null ? "" : (lf ? lp.ControlTypeA : lp.ControlTypeB);
|
||||
string rct = rp == null ? "" : (rf ? rp.ControlTypeB : rp.ControlTypeA);
|
||||
@@ -1196,7 +1082,6 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// Train totals — same Pascal mass rules as Scenarios (IncludeVehicleToMass + 200 t retry).
|
||||
private void UpdateTrainStats()
|
||||
{
|
||||
if (_editingTrainset is null)
|
||||
@@ -1315,16 +1200,13 @@ public partial class Depot : UserControl
|
||||
};
|
||||
card.PointerPressed += (_, e) =>
|
||||
{
|
||||
// left button only - right-click is reserved for the context popup
|
||||
|
||||
if (!e.GetCurrentPoint(card).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
// Buttons on the card (split / remove) handle their own clicks.
|
||||
|
||||
if (e.Source is Control src && src.GetVisualAncestors().OfType<Button>().Any())
|
||||
return;
|
||||
|
||||
// Update selection without RebuildConsist so the pressed card stays
|
||||
// alive for a possible drag. Browser sync waits until click/release
|
||||
// so BuildList does not run under an active drag capture.
|
||||
_selected = item;
|
||||
SyncStartingVehicle();
|
||||
RefreshCardSelectionChrome();
|
||||
@@ -1337,8 +1219,7 @@ public partial class Depot : UserControl
|
||||
DragDrop.SetAllowDrop(card, true);
|
||||
card.AddHandler(DragDrop.DragOverEvent, Consist_OnDragOver);
|
||||
card.AddHandler(DragDrop.DropEvent, (s, e) => ConsistCard_OnDrop(e, index, card));
|
||||
// right-click a vehicle: wagon number + load actions (copy load from the
|
||||
// previous vehicle, fill to the maximum possible load)
|
||||
|
||||
card.ContextMenu = BuildCardMenu(card, item);
|
||||
return card;
|
||||
}
|
||||
@@ -1356,16 +1237,12 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// Selects, in the left database browser, the category, class and list entry of
|
||||
// the given vehicle - so clicking a consist car reveals it in the database.
|
||||
private void SelectInBrowser(Dynamic car)
|
||||
{
|
||||
var texture = _db.TextureForSkin(car.SkinFile);
|
||||
if (texture is null)
|
||||
return;
|
||||
|
||||
// Prefer the set lead so multi-unit followers (hidden in the browser) still
|
||||
// select their lead texture / class.
|
||||
var set = _db.ResolveSet(texture);
|
||||
var browserTex = set is { Count: > 0 } ? set[0] : texture;
|
||||
|
||||
@@ -1377,8 +1254,6 @@ public partial class Depot : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
// Mirrors a list pick in the combos, then restores the selection that the
|
||||
// rebuilt list dropped.
|
||||
private void SyncCombosTo(VehicleTexture texture)
|
||||
{
|
||||
if (_syncingCombos || !ApplyFilters(CategoryOf(texture), ClassOf(texture)))
|
||||
@@ -1387,8 +1262,6 @@ public partial class Depot : UserControl
|
||||
PostSyncing(() => { BuildList(); SelectListEntry(texture); });
|
||||
}
|
||||
|
||||
// Runs work with the sync flag up, so the selections it makes do not come
|
||||
// back as another sync.
|
||||
private void WhileSyncing(Action work)
|
||||
{
|
||||
_syncingCombos = true;
|
||||
@@ -1396,8 +1269,6 @@ public partial class Depot : UserControl
|
||||
finally { _syncingCombos = false; }
|
||||
}
|
||||
|
||||
// Same, on the next dispatcher pass: clearing Items from inside a
|
||||
// SelectionChanged throws. False when a sync is already pending.
|
||||
private bool PostSyncing(Action work)
|
||||
{
|
||||
if (_syncingCombos)
|
||||
@@ -1421,7 +1292,6 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// The "#<n>" number written into the node name (e.g. EU07-005#11). 0 = none.
|
||||
private static int GetWagonNumber(Dynamic car)
|
||||
{
|
||||
string name = car.Name ?? "";
|
||||
@@ -1443,7 +1313,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Small right-click popup to type a wagon number for the vehicle.
|
||||
private void ShowWagonNumberFlyout(Control anchor, Dynamic car)
|
||||
{
|
||||
var num = new NumericUpDown
|
||||
@@ -1475,8 +1344,6 @@ public partial class Depot : UserControl
|
||||
return item.Cars.Count > 1 ? $"{key} [{item.Cars.Count}]" : key;
|
||||
}
|
||||
|
||||
// Coupling indicator between two units (or trailing after the last unit).
|
||||
// Click it to open a flyout with the 8-bit coupling editor for the unit's last car.
|
||||
private Control BuildCoupler(ConsistItem item, bool trailing = false)
|
||||
{
|
||||
var glyph = new TextBlock
|
||||
@@ -1599,7 +1466,6 @@ public partial class Depot : UserControl
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Brakes / Loads editors for the selected unit (applied to all of its cars).
|
||||
private void UpdateDetails()
|
||||
{
|
||||
brakesPanel.Children.Clear();
|
||||
@@ -1611,7 +1477,6 @@ public partial class Depot : UserControl
|
||||
: $"{App.Loc["SelectedVehicle"]}: " +
|
||||
(_selected.Grouped ? UnitLabel(_selected) : _selected.Cars[0].Name);
|
||||
|
||||
// whole-consist load tools sit at the top of the Loads tab, always available
|
||||
loadsPanel.Children.Add(BuildConsistLoadTools());
|
||||
|
||||
if (_selected is null)
|
||||
@@ -1627,9 +1492,6 @@ public partial class Depot : UserControl
|
||||
BuildDamageEditor(_selected);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- load tools
|
||||
|
||||
// The per-vehicle right-click menu: wagon number plus the two load shortcuts.
|
||||
private ContextMenu BuildCardMenu(Control anchor, ConsistItem item)
|
||||
{
|
||||
var menu = new ContextMenu();
|
||||
@@ -1662,7 +1524,6 @@ public partial class Depot : UserControl
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Button bar that applies a load operation to every vehicle in the consist.
|
||||
private Control BuildConsistLoadTools()
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) };
|
||||
@@ -1687,15 +1548,12 @@ public partial class Depot : UserControl
|
||||
return b;
|
||||
}
|
||||
|
||||
// True when the unit's lead car can actually carry cargo.
|
||||
private bool IsLoadable(ConsistItem item)
|
||||
{
|
||||
var p = PhysicsFor(item.Cars[0]);
|
||||
return p != null && (p.MaxLoad > 0 || !string.IsNullOrWhiteSpace(p.LoadAccepted));
|
||||
}
|
||||
|
||||
// The largest cargo this car accepts (.fiz MaxLoad), 1000 as a fallback for a
|
||||
// car that lists accepted cargo but no explicit maximum, 0 when it can't load.
|
||||
private int MaxLoadFor(Dynamic car)
|
||||
{
|
||||
var p = PhysicsFor(car);
|
||||
@@ -1704,7 +1562,6 @@ public partial class Depot : UserControl
|
||||
return string.IsNullOrWhiteSpace(p.LoadAccepted) ? 0 : 1000;
|
||||
}
|
||||
|
||||
// Cargo types this car accepts (.fiz LoadAccepted), else the global list.
|
||||
private List<string> AcceptedTypesFor(Dynamic car)
|
||||
{
|
||||
var p = PhysicsFor(car);
|
||||
@@ -1713,8 +1570,6 @@ public partial class Depot : UserControl
|
||||
return LoadTypes().ToList();
|
||||
}
|
||||
|
||||
// Copies the previous unit's load (type + amount) onto this unit, clamped to
|
||||
// each car's own maximum.
|
||||
private void CopyLoadFromPrevious(ConsistItem item)
|
||||
{
|
||||
int idx = _consist.IndexOf(item);
|
||||
@@ -1731,8 +1586,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Fills one unit's cars to their maximum load (keeping the cargo type, or
|
||||
// assigning the first accepted one when none is set yet).
|
||||
private void SetUnitMaxLoad(ConsistItem item)
|
||||
{
|
||||
foreach (var c in item.Cars)
|
||||
@@ -1740,7 +1593,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Random cargo type for every loadable car in the consist, filled to the max.
|
||||
private void RandomLoadTypeForConsist()
|
||||
{
|
||||
foreach (var c in _consist.SelectMany(u => u.Cars))
|
||||
@@ -1756,7 +1608,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Every loadable car in the consist filled to its maximum.
|
||||
private void MaxAmountForConsist()
|
||||
{
|
||||
foreach (var c in _consist.SelectMany(u => u.Cars))
|
||||
@@ -1764,9 +1615,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Random amount (per car) for every loadable car in the consist, keeping the
|
||||
// cargo type (assigning the first accepted one when none is set). Lets you roll
|
||||
// a different tonnage for each wagon with one click.
|
||||
private void RandomAmountForConsist()
|
||||
{
|
||||
foreach (var c in _consist.SelectMany(u => u.Cars))
|
||||
@@ -1783,7 +1631,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Fills a single car to its maximum load (no-op for non-loadable cars).
|
||||
private void FillToMax(Dynamic car)
|
||||
{
|
||||
int max = MaxLoadFor(car);
|
||||
@@ -1800,7 +1647,6 @@ public partial class Depot : UserControl
|
||||
Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
// Row of "<label> <control>".
|
||||
private static Control LabeledRow(string label, Control control)
|
||||
{
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
|
||||
@@ -1816,7 +1662,6 @@ public partial class Depot : UserControl
|
||||
private static void AddOption(ComboBox combo, string content, string? tag)
|
||||
=> combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag });
|
||||
|
||||
// Brake mode / load adaptation / switch, written into each car's coupling "B" param.
|
||||
private void BuildBrakesEditor(ConsistItem item)
|
||||
{
|
||||
var brake = item.Cars[0].Coupling.GetBrake();
|
||||
@@ -1863,7 +1708,6 @@ public partial class Depot : UserControl
|
||||
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo));
|
||||
}
|
||||
|
||||
// Descriptive labels (what the option does), per the wiki - not the raw code.
|
||||
private static string BrakeModeLabel(string code) => code switch
|
||||
{
|
||||
"G" => App.Loc["BrakeFreight"],
|
||||
@@ -1893,7 +1737,6 @@ public partial class Depot : UserControl
|
||||
_ => App.Loc["None"]
|
||||
};
|
||||
|
||||
// Wheel-damage editor: sway / flat-spot parameters (the "W" coupling prefix).
|
||||
private void BuildDamageEditor(ConsistItem item)
|
||||
{
|
||||
var w = item.Cars[0].Coupling.GetWheels() ?? new WheelSettings();
|
||||
@@ -1962,31 +1805,24 @@ public partial class Depot : UserControl
|
||||
damagePanel.Children.Add(grid);
|
||||
}
|
||||
|
||||
// Cargo type + amount, written into each car's node::dynamic trailing params.
|
||||
private void BuildLoadsEditor(ConsistItem item)
|
||||
{
|
||||
var lead = item.Cars[0];
|
||||
var phys = PhysicsFor(lead);
|
||||
|
||||
// Prefer the cargo this vehicle actually accepts (.fiz LoadAccepted);
|
||||
// otherwise offer everything from data/load_weights.txt.
|
||||
var available = phys != null && !string.IsNullOrWhiteSpace(phys.LoadAccepted)
|
||||
? phys.LoadAccepted.Split(',', ';').Select(s => s.Trim()).Where(s => s.Length > 0).ToList()
|
||||
: LoadTypes().ToList();
|
||||
|
||||
// Keep a load already set on the vehicle selectable even when it is not in
|
||||
// the accepted/known list (e.g. a custom load read from the scenery).
|
||||
if (!string.IsNullOrEmpty(lead.LoadType) &&
|
||||
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
|
||||
available.Insert(0, lead.LoadType!);
|
||||
|
||||
// Dropdown of available cargo: the leading "(none)" entry clears the load.
|
||||
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 200 };
|
||||
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
|
||||
foreach (var name in available)
|
||||
typeCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });
|
||||
|
||||
// Pre-select the vehicle's current load (or "(none)").
|
||||
typeCombo.SelectedIndex = 0;
|
||||
if (!string.IsNullOrEmpty(lead.LoadType))
|
||||
foreach (var obj in typeCombo.Items)
|
||||
@@ -1997,7 +1833,6 @@ public partial class Depot : UserControl
|
||||
break;
|
||||
}
|
||||
|
||||
// Cap the amount at the .fiz MaxLoad when it is defined.
|
||||
int maxLoad = phys != null && phys.MaxLoad > 0 ? phys.MaxLoad : 1000;
|
||||
var countBox = new NumericUpDown
|
||||
{
|
||||
@@ -2011,16 +1846,14 @@ public partial class Depot : UserControl
|
||||
int count = (int)(countBox.Value ?? 0);
|
||||
foreach (var c in item.Cars)
|
||||
{
|
||||
// "(none)" -> no cargo at all; otherwise store the chosen type/amount.
|
||||
|
||||
c.LoadType = string.IsNullOrEmpty(type) ? null : type;
|
||||
c.LoadCount = string.IsNullOrEmpty(type) ? 0 : count;
|
||||
if (c.LoadCount > 0)
|
||||
c.HasVelocity = true; // velocity token must precede loadcount
|
||||
c.HasVelocity = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Wire the handlers only after the initial selection so we don't apply
|
||||
// (and mark the consist dirty) while merely populating the editor.
|
||||
typeCombo.SelectionChanged += (_, _) => Apply();
|
||||
countBox.ValueChanged += (_, _) => Apply();
|
||||
|
||||
@@ -2028,9 +1861,6 @@ public partial class Depot : UserControl
|
||||
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
|
||||
}
|
||||
|
||||
// Cargo names + per-unit weights parsed once from data/load_weights.txt (the
|
||||
// file the simulator and the original Starter use). Each line is
|
||||
// "<name>: <weight>".
|
||||
private static List<string>? _loadTypes;
|
||||
private static Dictionary<string, int>? _loadWeights;
|
||||
|
||||
@@ -2060,7 +1890,7 @@ public partial class Depot : UserControl
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* missing / unreadable - just no suggestions */ }
|
||||
catch { }
|
||||
|
||||
_loadTypes = names.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -2074,18 +1904,12 @@ public partial class Depot : UserControl
|
||||
return _loadTypes!;
|
||||
}
|
||||
|
||||
// Per-unit weight (kg) of a cargo, defaulting to 1000 like the original Starter.
|
||||
private static int LoadWeight(string? name)
|
||||
{
|
||||
EnsureLoads();
|
||||
return !string.IsNullOrEmpty(name) && _loadWeights!.TryGetValue(name!, out int w) ? w : 1000;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- minis
|
||||
|
||||
// Decodes the miniature down to the requested display height and caches it,
|
||||
// so the same skin is never decoded twice and full-resolution bitmaps never
|
||||
// stay resident in memory.
|
||||
private Bitmap? GetMiniBitmap(string? name, int height)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
@@ -2110,8 +1934,6 @@ public partial class Depot : UserControl
|
||||
return bmp;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ events
|
||||
|
||||
private void SearchBox_OnTextChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (_suppress || _searchTimer is null) return;
|
||||
@@ -2268,8 +2090,7 @@ public partial class Depot : UserControl
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
},
|
||||
// DockPanel: in a horizontal StackPanel the caption gets infinite
|
||||
// width, never wraps and shoves the spinner off the edge.
|
||||
|
||||
new DockPanel
|
||||
{
|
||||
LastChildFill = true,
|
||||
@@ -2402,7 +2223,6 @@ public partial class Depot : UserControl
|
||||
private void RandomOrientButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) =>
|
||||
RandomizeOrientation();
|
||||
|
||||
// Shuffle wagon order from the selected unit to the end (multi-unit groups stay put).
|
||||
private void RandomizeOrder()
|
||||
{
|
||||
int start = _selected != null ? _consist.IndexOf(_selected) : 0;
|
||||
@@ -2433,7 +2253,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// Randomly flip units from the selected one to the end of the consist.
|
||||
private void RandomizeOrientation()
|
||||
{
|
||||
int start = _selected != null ? _consist.IndexOf(_selected) : 0;
|
||||
@@ -2448,8 +2267,6 @@ public partial class Depot : UserControl
|
||||
RebuildConsist();
|
||||
}
|
||||
|
||||
// ── drag / drop (browser → consist, reorder inside consist) ────────────
|
||||
|
||||
private void MiniPreview_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (_browserSelected is null || !e.GetCurrentPoint(miniPreviewPanel).Properties.IsLeftButtonPressed)
|
||||
@@ -2461,7 +2278,7 @@ public partial class Depot : UserControl
|
||||
{
|
||||
if (!e.GetCurrentPoint(vehicleListBox).Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
// Resolve the row under the pointer so a drag can start before selection settles.
|
||||
|
||||
var hit = e.Source as Control;
|
||||
while (hit != null && hit is not ListBoxItem)
|
||||
hit = hit.Parent as Control;
|
||||
@@ -2514,7 +2331,7 @@ public partial class Depot : UserControl
|
||||
var press = _pendingDragPress;
|
||||
var texture = _pendingDragTexture;
|
||||
int consistIndex = _pendingDragConsistIndex;
|
||||
// Keep browser sync cancelled for the drag session; click path uses it.
|
||||
|
||||
_pendingBrowserSync = null;
|
||||
_pendingDragPress = null;
|
||||
_dragSessionActive = true;
|
||||
@@ -2565,14 +2382,14 @@ public partial class Depot : UserControl
|
||||
{
|
||||
var sync = _pendingBrowserSync;
|
||||
ClearPendingDrag();
|
||||
// Plain click on a consist card: reveal it in the vehicle browser.
|
||||
|
||||
if (sync is { Cars.Count: > 0 })
|
||||
SelectInBrowser(sync.Cars[0]);
|
||||
}
|
||||
|
||||
private void Consist_OnDragOver(object? sender, DragEventArgs e)
|
||||
{
|
||||
// Payload is kept in fields set before DoDragDropAsync; formats are markers.
|
||||
|
||||
if (_dragTexture != null || _dragConsistIndex >= 0)
|
||||
{
|
||||
e.DragEffects = _dragConsistIndex >= 0 ? DragDropEffects.Move : DragDropEffects.Copy;
|
||||
@@ -2621,7 +2438,7 @@ public partial class Depot : UserControl
|
||||
private void MoveConsistUnit(int from, int to)
|
||||
{
|
||||
if (from < 0 || from >= _consist.Count) return;
|
||||
if (to > from) to--; // account for removal shifting indices
|
||||
if (to > from) to--;
|
||||
to = Math.Clamp(to, 0, _consist.Count - 1);
|
||||
if (from == to) return;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ public enum MessageBoxButtons
|
||||
|
||||
public static class MessageBox
|
||||
{
|
||||
/// <summary>Simple Yes/No dialog. Returns true for Yes.</summary>
|
||||
|
||||
public static async Task<bool> Show(Window owner, string message, string title, MessageBoxButtons _)
|
||||
{
|
||||
var win = new Window
|
||||
|
||||
@@ -22,26 +22,20 @@ public partial class Scenarios : UserControl
|
||||
{
|
||||
public List<Scenery> 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;
|
||||
// "Today" season: the day field only previews the current day-of-year; the
|
||||
// stored Day stays 0 so the scenery keeps using the live date.
|
||||
|
||||
private bool _todaySeason;
|
||||
|
||||
// suppresses the list-filter handlers while their state is restored at startup
|
||||
private bool _loadingFilters;
|
||||
|
||||
public Scenarios()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Sceneries are parsed once at startup (behind the splash).
|
||||
Sceneries = GameData.Instance.Sceneries;
|
||||
|
||||
// Restore the persisted list filters before building the tree/list. Guarded
|
||||
// so assigning IsChecked here doesn't run the rebuild handlers prematurely.
|
||||
_loadingFilters = true;
|
||||
showAiCheck.IsChecked = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
|
||||
drivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
|
||||
@@ -51,9 +45,6 @@ public partial class Scenarios : UserControl
|
||||
BuildSceneryTree();
|
||||
RestoreLastScenery();
|
||||
|
||||
// refresh the consist preview when the view is shown again (e.g. after
|
||||
// editing the consist in the depot). Switching tabs only toggles IsVisible
|
||||
// (the view stays in the tree), so listen for that too.
|
||||
AttachedToVisualTree += (_, _) => OnReentered();
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
@@ -62,20 +53,12 @@ public partial class Scenarios : UserControl
|
||||
};
|
||||
}
|
||||
|
||||
// Called whenever this view is shown again (tab switch / re-attach). Edits made
|
||||
// in the depot mutate the same Trainset objects, so refresh both the consist
|
||||
// preview and the vehicle-list captions that depend on them.
|
||||
private void OnReentered()
|
||||
{
|
||||
RefreshVehicleLabels();
|
||||
RefreshSelectedConsist();
|
||||
}
|
||||
|
||||
// (Re)builds the scenery tree, honouring the "show archival" switch. Archival
|
||||
// sceneries (those declaring //$a) are skipped when the switch is off; group
|
||||
// folders that end up empty are omitted. The Tag keeps each node's original
|
||||
// scenery index, so the vehicles list stays aligned. Top-level nodes and group
|
||||
// children are sorted alphabetically.
|
||||
private void BuildSceneryTree()
|
||||
{
|
||||
sceneryList.Items.Clear();
|
||||
@@ -136,8 +119,6 @@ public partial class Scenarios : UserControl
|
||||
sceneryList.Items.Add(item);
|
||||
}
|
||||
|
||||
// Restores the scenery selected last session (starter.last.scenery).
|
||||
// Like Pascal ScenariosList: fall back to the first leaf when nothing matches.
|
||||
private void RestoreLastScenery()
|
||||
{
|
||||
string last = StarterNG.Classes.Settings.Instance.LastScenery;
|
||||
@@ -208,7 +189,7 @@ public partial class Scenarios : UserControl
|
||||
|
||||
private void PersistLastScenery(Scenery scenery)
|
||||
{
|
||||
// Pascal InitSCN stores the tree node text (//$n or file name).
|
||||
|
||||
string name = scenery.DisplayName;
|
||||
if (string.Equals(StarterNG.Classes.Settings.Instance.LastScenery, name, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
@@ -228,10 +209,6 @@ public partial class Scenarios : UserControl
|
||||
TextWrapping = Avalonia.Media.TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
// Fills the Vehicles list for a scenery, honouring the two filters:
|
||||
// - "AI vehicles" off hides consists whose //$o description starts with '-';
|
||||
// - "Drivable only" on hides decoration consists (the //$decor flag).
|
||||
// Empty trainsets stay visible (label shows track), matching the Pascal starter.
|
||||
private void PopulateVehicleList(Scenery scn)
|
||||
{
|
||||
vehicleList.Items.Clear();
|
||||
@@ -251,7 +228,7 @@ public partial class Scenarios : UserControl
|
||||
Content = TrainsetListContent(trainset),
|
||||
Tag = i
|
||||
};
|
||||
// right-click: save/load to the warehouse + clipboard copy/paste
|
||||
|
||||
listItem.ContextMenu = ConsistContextMenu.Build(
|
||||
listItem,
|
||||
() => trainset,
|
||||
@@ -263,13 +240,10 @@ public partial class Scenarios : UserControl
|
||||
vehicleList.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
// A computer-driven consist marks its //$o description with a leading '-'.
|
||||
private static bool IsAiTrainset(Trainset trainset) =>
|
||||
!string.IsNullOrEmpty(trainset.Description) &&
|
||||
trainset.Description.TrimStart().StartsWith("-", StringComparison.Ordinal);
|
||||
|
||||
// AI/player vehicle filter toggled: persist the choice and rebuild the list
|
||||
// for the currently selected scenery.
|
||||
private void VehicleFilter_OnChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_loadingFilters) return;
|
||||
@@ -282,7 +256,6 @@ public partial class Scenarios : UserControl
|
||||
PopulateVehicleList(Sceneries[tag]);
|
||||
}
|
||||
|
||||
// Archival-scenery switch toggled: persist the choice and rebuild the tree.
|
||||
private void ArchivalSwitch_OnChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_loadingFilters) return;
|
||||
@@ -294,8 +267,6 @@ public partial class Scenarios : UserControl
|
||||
RestoreLastScenery();
|
||||
}
|
||||
|
||||
// Replaces a consist's vehicles (from a loaded warehouse preset or a pasted
|
||||
// clipboard consist) and refreshes the row caption / consist preview.
|
||||
private void ApplyConsist(ListBoxItem item, Trainset trainset, List<Dynamic> vehicles)
|
||||
{
|
||||
trainset.Vehicles = vehicles;
|
||||
@@ -304,9 +275,6 @@ public partial class Scenarios : UserControl
|
||||
ShowConsist(trainset);
|
||||
}
|
||||
|
||||
// Rebuilds the caption of each entry in the Vehicles list from its trainset's
|
||||
// current vehicles, so a consist edited in the depot is reflected here too.
|
||||
// Selection and order are preserved (only the text is updated).
|
||||
private void RefreshVehicleLabels()
|
||||
{
|
||||
if (sceneryList.SelectedItem is not TreeViewItem { Tag: int tag } || tag >= Sceneries.Count)
|
||||
@@ -327,11 +295,11 @@ public partial class Scenarios : UserControl
|
||||
{
|
||||
foreach (TreeViewItem? toCollapse in sceneryList.Items)
|
||||
{
|
||||
if (toCollapse != null)
|
||||
if (toCollapse != null)
|
||||
toCollapse.IsExpanded = false;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
if (item != null)
|
||||
item.IsExpanded = true;
|
||||
if (item is { Items.Count: > 0 })
|
||||
sceneryList.SelectedItem = item.Items[0];
|
||||
@@ -339,20 +307,16 @@ public partial class Scenarios : UserControl
|
||||
}
|
||||
Scenery selectedScn = Sceneries[tag];
|
||||
|
||||
// scenery-level info (distinct from the per-consist scenario description)
|
||||
ShowSceneryInfo(selectedScn);
|
||||
ShowWeather(selectedScn);
|
||||
PersistLastScenery(selectedScn);
|
||||
|
||||
// no consist chosen yet for this scenery
|
||||
missionDescription.Text = "";
|
||||
timetableContent.Text = App.Loc["NoTimetable"];
|
||||
|
||||
// add the (filtered) trainsets to the Vehicles list
|
||||
PopulateVehicleList(selectedScn);
|
||||
}
|
||||
|
||||
// Shows the selected scenery's description (//$d) and 1:1 image (//$i).
|
||||
private void ShowSceneryInfo(Scenery scenery)
|
||||
{
|
||||
SceneryI18n.LoadFor(scenery, App.Loc.CurrentLangCode);
|
||||
@@ -471,10 +435,9 @@ public partial class Scenarios : UserControl
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(full) { UseShellExecute = true });
|
||||
}
|
||||
catch { /* no handler */ }
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Rebuild tree labels after UI / scenery i18n language change.</summary>
|
||||
public void RebuildAfterLanguageChange()
|
||||
{
|
||||
string? selected = AppState.Instance.CurrentScenery?.Path;
|
||||
@@ -487,7 +450,7 @@ public partial class Scenarios : UserControl
|
||||
}
|
||||
if (AppState.Instance.CurrentScenery != null)
|
||||
ShowSceneryInfo(AppState.Instance.CurrentScenery);
|
||||
RefreshSelectedConsist(); // stats labels (length / mass / track)
|
||||
RefreshSelectedConsist();
|
||||
}
|
||||
|
||||
private async void ReloadSceneries_OnClick(object? sender, RoutedEventArgs e)
|
||||
@@ -528,19 +491,17 @@ public partial class Scenarios : UserControl
|
||||
|
||||
private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
// get selected scenery
|
||||
|
||||
var tItem = sceneryList.SelectedItem as TreeViewItem;
|
||||
if (tItem?.Tag is not int tTag)
|
||||
return;
|
||||
Scenery selectedScn = Sceneries[tTag];
|
||||
|
||||
// get selected trainset
|
||||
ListBoxItem? vItem = vehicleList.SelectedItem as ListBoxItem;
|
||||
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;
|
||||
AppState.Instance.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(selectedTrainset);
|
||||
@@ -549,9 +510,6 @@ public partial class Scenarios : UserControl
|
||||
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;
|
||||
@@ -565,7 +523,7 @@ public partial class Scenarios : UserControl
|
||||
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
|
||||
SelectByTag(weatherSeason, scenery.Day.ToString());
|
||||
SelectByTag(weatherOvercast, scenery.Overcast.ToString(CultureInfo.InvariantCulture));
|
||||
// Day 0 maps to the "Today" season entry: preview the live day-of-year.
|
||||
|
||||
ApplyTodayPreview(scenery.Day == 0);
|
||||
UpdateWeatherLabels();
|
||||
}
|
||||
@@ -575,7 +533,6 @@ public partial class Scenarios : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the form back into the current scenery and flags it for export rewrite.
|
||||
private void CaptureWeather()
|
||||
{
|
||||
if (_loadingWeather || _currentScenery is null)
|
||||
@@ -584,8 +541,7 @@ public partial class Scenarios : UserControl
|
||||
var scn = _currentScenery;
|
||||
if (weatherTime.SelectedTime is { } t)
|
||||
scn.ScenarioTimeOverride = $"{t.Hours:D2}:{t.Minutes:D2}";
|
||||
// In "Today" mode the day field is only a preview, so keep the stored day
|
||||
// at 0 (the scenery then follows the live date).
|
||||
|
||||
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
|
||||
scn.Temperature = weatherTemp.Value;
|
||||
scn.FogEnd = (int)weatherFog.Value;
|
||||
@@ -614,18 +570,14 @@ public partial class Scenarios : UserControl
|
||||
combo.SelectedItem = null;
|
||||
}
|
||||
|
||||
// --- weather form event handlers ---
|
||||
private void WeatherTime_OnChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) => CaptureWeather();
|
||||
|
||||
// "Current time" button: drop the live wall-clock time into the picker (the
|
||||
// SelectedTimeChanged handler then captures it onto the scenery).
|
||||
private void WeatherTimeNow_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
weatherTime.SelectedTime = new TimeSpan(now.Hour, now.Minute, 0);
|
||||
}
|
||||
|
||||
// Pascal actRestoreWeather — reload weather fields from the SCN snapshot.
|
||||
private void WeatherRestore_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_currentScenery is null) return;
|
||||
@@ -633,8 +585,6 @@ public partial class Scenarios : UserControl
|
||||
ShowWeather(_currentScenery);
|
||||
}
|
||||
|
||||
// Parses a stored "hh:mm" string into a TimeSpan, or null when it is missing
|
||||
// or malformed (so the picker simply shows no selection).
|
||||
private static TimeSpan? ParseTime(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
@@ -647,8 +597,6 @@ public partial class Scenarios : UserControl
|
||||
return null;
|
||||
}
|
||||
|
||||
// Toggles "Today" preview: the day field shows the live day-of-year (read-only)
|
||||
// and CaptureWeather keeps the stored Day at 0; any other season re-enables it.
|
||||
private void ApplyTodayPreview(bool today)
|
||||
{
|
||||
_todaySeason = today;
|
||||
@@ -668,7 +616,6 @@ public partial class Scenarios : UserControl
|
||||
|
||||
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)
|
||||
@@ -676,7 +623,7 @@ public partial class Scenarios : UserControl
|
||||
if ((weatherSeason.SelectedItem as ComboBoxItem)?.Tag is string tag &&
|
||||
int.TryParse(tag, out int day))
|
||||
{
|
||||
if (day == 0) // "Today" - preview the live day-of-year, keep stored day at 0
|
||||
if (day == 0)
|
||||
{
|
||||
ApplyTodayPreview(true);
|
||||
}
|
||||
@@ -691,9 +638,6 @@ public partial class Scenarios : UserControl
|
||||
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);
|
||||
@@ -713,15 +657,14 @@ public partial class Scenarios : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
string scnDir = Path.GetDirectoryName(scenery.Path) ?? ".";
|
||||
string root = Path.GetDirectoryName(scnDir) ?? ".";
|
||||
|
||||
var candidates = new[]
|
||||
{
|
||||
@@ -734,8 +677,6 @@ public partial class Scenarios : UserControl
|
||||
return candidates.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
// Renders the consist preview for a trainset (re-reads its current vehicles
|
||||
// so depot edits are reflected).
|
||||
private void ShowConsist(Trainset trainset)
|
||||
{
|
||||
consistStack.Children.Clear();
|
||||
@@ -743,11 +684,11 @@ public partial class Scenarios : UserControl
|
||||
var db = GameData.Instance.Vehicles;
|
||||
foreach (var train in trainset.Vehicles)
|
||||
{
|
||||
// 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)
|
||||
continue; // fallback: no mini
|
||||
continue;
|
||||
|
||||
int thumbH = StarterNG.Classes.Settings.Instance.LargeThumbnails ? 64 : 32;
|
||||
consistStack.Children.Add(new Image
|
||||
@@ -783,8 +724,6 @@ public partial class Scenarios : UserControl
|
||||
car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory);
|
||||
}
|
||||
|
||||
// When returning to this view, refresh the preview of the selected consist
|
||||
// in case it was modified in the depot.
|
||||
private void RefreshSelectedConsist()
|
||||
{
|
||||
if (AppState.Instance.CurrentTrainset is { } trainset)
|
||||
|
||||
@@ -13,7 +13,7 @@ using Avalonia.Markup.Xaml.Styling;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Classes;
|
||||
using StarterNG.Services;
|
||||
// Disambiguate from Avalonia.Input.KeyBinding (a command-input binding) imported above.
|
||||
|
||||
using KeyBinding = StarterNG.Classes.KeyBinding;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
@@ -36,23 +36,17 @@ public partial class Settings : UserControl
|
||||
};
|
||||
|
||||
FillLanguageCombo();
|
||||
// The other picker is in the main window's top bar; both follow whatever
|
||||
// language ends up loaded.
|
||||
|
||||
App.Loc.LanguageChanged += SelectActiveLanguage;
|
||||
|
||||
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
|
||||
FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb);
|
||||
|
||||
// The settings instance pulls live values from here whenever the app
|
||||
// closes or the game is launched.
|
||||
StarterNG.Classes.Settings.Instance.CaptureFromUi = ReadFromUi;
|
||||
|
||||
PopulateProfileCombo();
|
||||
ApplyToUi();
|
||||
|
||||
// Controls tab: load the key bindings and build the editor + on-screen
|
||||
// keyboard. The tunnelling handler lets us capture a key press before the
|
||||
// focused button consumes it (Space/Enter would otherwise click it).
|
||||
KeyboardConfig.Instance.Load();
|
||||
BuildControlsTab();
|
||||
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
|
||||
@@ -130,7 +124,6 @@ public partial class Settings : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings instance → controls ──────────────────────────────────────
|
||||
public void ReloadFromSettings() => ApplyToUi();
|
||||
|
||||
private void ApplyToUi()
|
||||
@@ -142,7 +135,6 @@ public partial class Settings : UserControl
|
||||
PopulateProfileCombo();
|
||||
PopulateExeCombo();
|
||||
|
||||
// General
|
||||
SelectActiveLanguage();
|
||||
FullscreenCb.IsChecked = s.Fullscreen;
|
||||
PauseInactiveCb.IsChecked = s.PauseWhenInactive;
|
||||
@@ -151,7 +143,6 @@ public partial class Settings : UserControl
|
||||
MouseHorInvertCb.IsChecked = s.InvertMouseHorizontal;
|
||||
MouseVertInvertCb.IsChecked = s.InvertMouseVertical;
|
||||
|
||||
// Communication
|
||||
GamepadIgnoreCb.IsChecked = s.IgnoreGamepad;
|
||||
FeedbackCb.SelectedIndex = s.FeedbackMode;
|
||||
FeedbackPortNud.Value = (decimal)s.FeedbackPort;
|
||||
@@ -167,7 +158,6 @@ public partial class Settings : UserControl
|
||||
UartRadioChannelCb.IsChecked = s.UartRadioChannel;
|
||||
UpdateFeedbackDetailVisibility();
|
||||
|
||||
// Other
|
||||
SelectExeCb.SelectedIndex = s.SelectExeAutomatically
|
||||
? 0
|
||||
: Math.Max(0, FindComboIndexByContent(SelectExeCb, s.ExecutablePath));
|
||||
@@ -175,7 +165,6 @@ public partial class Settings : UserControl
|
||||
VirtualShuntingCb.IsChecked = s.VirtualShunting;
|
||||
LogMissingVehicleFilesCb.IsChecked = s.LogMissingVehicleFiles;
|
||||
|
||||
// Graphics
|
||||
RenderEngineCb.SelectedIndex = s.RenderEngine;
|
||||
SelectResolution(s.Width, s.Height);
|
||||
bufferScale.Value = s.BufferScalePercent;
|
||||
@@ -210,7 +199,6 @@ public partial class Settings : UserControl
|
||||
FullscreenWindowedCb.IsChecked = s.FullscreenWindowed;
|
||||
RenderAngleVulkanCb.IsChecked = s.RenderAngleVulkan;
|
||||
|
||||
// Physics
|
||||
TrackCurvesSlider.Value = s.SplineFidelity;
|
||||
PhysicsAccuracyCb.IsChecked = s.FullPhysics;
|
||||
PantographBreakCb.IsChecked = s.EnableTraction;
|
||||
@@ -224,7 +212,6 @@ public partial class Settings : UserControl
|
||||
BrakeStepSlider.Value = s.BrakeStep;
|
||||
BrakeSpeedSlider.Value = s.BrakeSpeed;
|
||||
|
||||
// Sound
|
||||
EnableSoundsCb.IsChecked = s.SoundEnabled;
|
||||
VolumeSlider.Value = s.Volume;
|
||||
RadioVolumeSlider.Value = s.RadioVolume;
|
||||
@@ -232,7 +219,6 @@ public partial class Settings : UserControl
|
||||
PositionalVolumeSlider.Value = s.PositionalVolume;
|
||||
AmbientVolumeSlider.Value = s.AmbientVolume;
|
||||
|
||||
// Advanced
|
||||
CompressTexturesCb.IsChecked = s.CompressTextures;
|
||||
ScaleSpecularsCb.IsChecked = s.ScaleSpeculars;
|
||||
UseGLESCb.IsChecked = s.UseGLES;
|
||||
@@ -244,7 +230,6 @@ public partial class Settings : UserControl
|
||||
TrainPhysicsThreadsSlider.Value = s.TrainPhysicsThreads;
|
||||
IgnoreIrrelevantTrainsCb.IsChecked = s.IgnoreIrrelevantTrains;
|
||||
|
||||
// Starter
|
||||
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
|
||||
LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
|
||||
AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree;
|
||||
@@ -257,14 +242,10 @@ public partial class Settings : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// ── controls → Settings instance ──────────────────────────────────────
|
||||
private void ReadFromUi()
|
||||
{
|
||||
var s = StarterNG.Classes.Settings.Instance;
|
||||
|
||||
// General
|
||||
// Keep the loaded language rather than writing a wrong one over it when
|
||||
// the combo has no valid pick.
|
||||
s.Language = (ChangeLanguageCb.SelectedItem as ComboBoxItem)?.Content?.ToString()
|
||||
?? App.Loc.CurrentLanguage;
|
||||
s.Fullscreen = IsChecked(FullscreenCb);
|
||||
@@ -274,7 +255,6 @@ public partial class Settings : UserControl
|
||||
s.InvertMouseHorizontal = IsChecked(MouseHorInvertCb);
|
||||
s.InvertMouseVertical = IsChecked(MouseVertInvertCb);
|
||||
|
||||
// Communication
|
||||
s.IgnoreGamepad = IsChecked(GamepadIgnoreCb);
|
||||
s.FeedbackMode = Math.Max(0, FeedbackCb.SelectedIndex);
|
||||
s.FeedbackPort = (int)(FeedbackPortNud.Value ?? 888);
|
||||
@@ -289,7 +269,6 @@ public partial class Settings : UserControl
|
||||
s.UartRadioVolume = IsChecked(UartRadioVolumeCb);
|
||||
s.UartRadioChannel = IsChecked(UartRadioChannelCb);
|
||||
|
||||
// Other
|
||||
s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0;
|
||||
s.ExecutablePath = s.SelectExeAutomatically ? "eu07.exe"
|
||||
: (SelectExeCb.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "eu07.exe";
|
||||
@@ -297,7 +276,6 @@ public partial class Settings : UserControl
|
||||
s.VirtualShunting = IsChecked(VirtualShuntingCb);
|
||||
s.LogMissingVehicleFiles = IsChecked(LogMissingVehicleFilesCb);
|
||||
|
||||
// Graphics
|
||||
s.RenderEngine = Math.Max(0, RenderEngineCb.SelectedIndex);
|
||||
ReadResolution(s);
|
||||
s.BufferScalePercent = (int)bufferScale.Value;
|
||||
@@ -332,7 +310,6 @@ public partial class Settings : UserControl
|
||||
s.FullscreenWindowed = IsChecked(FullscreenWindowedCb);
|
||||
s.RenderAngleVulkan = IsChecked(RenderAngleVulkanCb);
|
||||
|
||||
// Physics
|
||||
s.SplineFidelity = (int)TrackCurvesSlider.Value;
|
||||
s.FullPhysics = IsChecked(PhysicsAccuracyCb);
|
||||
s.EnableTraction = IsChecked(PantographBreakCb);
|
||||
@@ -346,7 +323,6 @@ public partial class Settings : UserControl
|
||||
s.BrakeStep = BrakeStepSlider.Value;
|
||||
s.BrakeSpeed = BrakeSpeedSlider.Value;
|
||||
|
||||
// Sound
|
||||
s.SoundEnabled = IsChecked(EnableSoundsCb);
|
||||
s.Volume = (int)VolumeSlider.Value;
|
||||
s.RadioVolume = (int)RadioVolumeSlider.Value;
|
||||
@@ -354,7 +330,6 @@ public partial class Settings : UserControl
|
||||
s.PositionalVolume = (int)PositionalVolumeSlider.Value;
|
||||
s.AmbientVolume = (int)AmbientVolumeSlider.Value;
|
||||
|
||||
// Advanced
|
||||
s.CompressTextures = IsChecked(CompressTexturesCb);
|
||||
s.ScaleSpeculars = IsChecked(ScaleSpecularsCb);
|
||||
s.UseGLES = IsChecked(UseGLESCb);
|
||||
@@ -366,7 +341,6 @@ public partial class Settings : UserControl
|
||||
s.TrainPhysicsThreads = (int)TrainPhysicsThreadsSlider.Value;
|
||||
s.IgnoreIrrelevantTrains = IsChecked(IgnoreIrrelevantTrainsCb);
|
||||
|
||||
// Starter
|
||||
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
|
||||
s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
|
||||
s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb);
|
||||
@@ -395,7 +369,6 @@ public partial class Settings : UserControl
|
||||
SaveStatus.Text = string.Empty;
|
||||
}
|
||||
|
||||
// ── resolution combo helpers ──────────────────────────────────────────
|
||||
private void SelectResolution(int width, int height)
|
||||
{
|
||||
string target = $"{width}x{height}";
|
||||
@@ -407,7 +380,7 @@ public partial class Settings : UserControl
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Not in the predefined list: add and select it.
|
||||
|
||||
var added = new ComboBoxItem { Content = target };
|
||||
ResolutionCb.Items.Add(added);
|
||||
ResolutionCb.SelectedItem = added;
|
||||
@@ -427,7 +400,6 @@ public partial class Settings : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// ── existing slider readouts ──────────────────────────────────────────
|
||||
private void TextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (texResolution is null || textureResolutionSlider is null)
|
||||
@@ -455,8 +427,6 @@ public partial class Settings : UserControl
|
||||
shaderResolution.Text = $"{resolution} px";
|
||||
}
|
||||
|
||||
// One entry per file in the lang/ folder, so every translation is reachable
|
||||
// (the list used to be a hard-coded Polski / English pair).
|
||||
private void FillLanguageCombo()
|
||||
{
|
||||
ChangeLanguageCb.Items.Clear();
|
||||
@@ -467,8 +437,6 @@ public partial class Settings : UserControl
|
||||
SelectActiveLanguage();
|
||||
}
|
||||
|
||||
// Points the combo at the language currently loaded. The selection change
|
||||
// lands in the handler below, which sees it is already loaded and stops.
|
||||
private void SelectActiveLanguage()
|
||||
{
|
||||
var item = ChangeLanguageCb.Items.OfType<ComboBoxItem>()
|
||||
@@ -499,7 +467,6 @@ public partial class Settings : UserControl
|
||||
UartExpander.IsVisible = FeedbackCb.SelectedIndex == 5;
|
||||
}
|
||||
|
||||
// ── tiny helpers ──────────────────────────────────────────────────────
|
||||
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
|
||||
|
||||
private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
|
||||
@@ -517,29 +484,20 @@ public partial class Settings : UserControl
|
||||
for (int i = 0; i < steps.Length; i++)
|
||||
if (steps[i] == anisotropy)
|
||||
return i + 1;
|
||||
return 4; // default → 8x
|
||||
return 4;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Controls tab — key-binding editor and on-screen keyboard
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private TextBox? _controlsSearch;
|
||||
private StackPanel? _bindingListPanel;
|
||||
private StackPanel? _keyboardPanel;
|
||||
|
||||
// Binding currently waiting for a key press, and the button that launched it.
|
||||
private KeyBinding? _capturing;
|
||||
private Button? _capturingButton;
|
||||
|
||||
// On-screen key cells, keyed by token, so colours can be refreshed in place
|
||||
// (rebuilding the whole keyboard would detach an open assignment flyout).
|
||||
private readonly Dictionary<string, Grid> _keyCells = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Guards the assignment flyout's combo boxes against feedback during programmatic updates.
|
||||
private bool _flyoutLoading;
|
||||
|
||||
// State colours (match the legend): unassigned/filler, plain, +shift, +ctrl.
|
||||
private static readonly IBrush KeyUnassignedBrush = new SolidColorBrush(Color.Parse("#2A3036"));
|
||||
private static readonly IBrush KeyFillerBrush = new SolidColorBrush(Color.Parse("#21262B"));
|
||||
private static readonly IBrush KeyPlainBrush = new SolidColorBrush(Color.Parse("#2E9E1F"));
|
||||
@@ -550,7 +508,7 @@ public partial class Settings : UserControl
|
||||
private static readonly IBrush FgDimBrush = new SolidColorBrush(Color.Parse("#9098A0"));
|
||||
private static readonly IBrush ConflictBrush = new SolidColorBrush(Color.Parse("#E06C5A"));
|
||||
|
||||
private const double KeyUnit = 30; // px per relative width unit
|
||||
private const double KeyUnit = 30;
|
||||
private const double KeyHeight = 34;
|
||||
private const double KeyGap = 4;
|
||||
|
||||
@@ -559,7 +517,6 @@ public partial class Settings : UserControl
|
||||
if (ControlsHost is null)
|
||||
return;
|
||||
|
||||
// Row 0 — search / restore bar.
|
||||
var topBar = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Thickness(0, 0, 0, 8) };
|
||||
topBar.Children.Add(new TextBlock
|
||||
{
|
||||
@@ -585,7 +542,6 @@ public partial class Settings : UserControl
|
||||
Grid.SetRow(topBar, 0);
|
||||
ControlsHost.Children.Add(topBar);
|
||||
|
||||
// Row 1 — scrollable binding list.
|
||||
_bindingListPanel = new StackPanel { Spacing = 3 };
|
||||
var listScroll = new ScrollViewer
|
||||
{
|
||||
@@ -595,7 +551,6 @@ public partial class Settings : UserControl
|
||||
Grid.SetRow(listScroll, 1);
|
||||
ControlsHost.Children.Add(listScroll);
|
||||
|
||||
// Row 2 — legend + on-screen keyboard.
|
||||
var bottom = new StackPanel { Spacing = 8, Margin = new Thickness(0, 10, 0, 0) };
|
||||
bottom.Children.Add(BuildLegend());
|
||||
_keyboardPanel = new StackPanel();
|
||||
@@ -612,7 +567,6 @@ public partial class Settings : UserControl
|
||||
RebuildKeyboard();
|
||||
}
|
||||
|
||||
// ── binding list ───────────────────────────────────────────────────────
|
||||
private void RebuildBindingList()
|
||||
{
|
||||
if (_bindingListPanel is null)
|
||||
@@ -707,17 +661,16 @@ public partial class Settings : UserControl
|
||||
};
|
||||
}
|
||||
|
||||
// ── key capture ──────────────────────────────────────────────────────--
|
||||
private void StartCapture(KeyBinding b, Button button)
|
||||
{
|
||||
// Restore the previously listening button, if any, before switching.
|
||||
|
||||
if (_capturing is not null && _capturingButton is not null)
|
||||
_capturingButton.Content = ComboText(_capturing);
|
||||
|
||||
_capturing = b;
|
||||
_capturingButton = button;
|
||||
button.Content = App.Loc["PressKey"];
|
||||
button.Focus(); // keep focus inside the view so the tunnel handler sees keys
|
||||
button.Focus();
|
||||
}
|
||||
|
||||
private void CancelCapture()
|
||||
@@ -740,14 +693,13 @@ public partial class Settings : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for a real key while only modifiers are held.
|
||||
if (KeyMap.IsModifierKey(e.Key))
|
||||
return;
|
||||
|
||||
string? token = KeyMap.FromInput(e.Key, e.PhysicalKey);
|
||||
if (token is null)
|
||||
{
|
||||
e.Handled = true; // swallow unsupported keys instead of clicking the button
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -763,7 +715,6 @@ public partial class Settings : UserControl
|
||||
RebuildKeyboard();
|
||||
}
|
||||
|
||||
// ── on-screen keyboard ───────────────────────────────────────────────--
|
||||
private void RebuildKeyboard()
|
||||
{
|
||||
if (_keyboardPanel is null)
|
||||
@@ -780,15 +731,13 @@ public partial class Settings : UserControl
|
||||
_keyboardPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
// Recolours every key in place without rebuilding the tree, so an open
|
||||
// assignment flyout keeps its anchor.
|
||||
private void UpdateKeyboardColors()
|
||||
{
|
||||
var states = ComputeKeyStates();
|
||||
foreach (var (token, content) in _keyCells)
|
||||
{
|
||||
if (content.Children.Count > 0)
|
||||
content.Children.RemoveAt(0); // drop the old background, keep the label
|
||||
content.Children.RemoveAt(0);
|
||||
content.Children.Insert(0, BuildKeyBackground(token, states));
|
||||
states.TryGetValue(token, out var state);
|
||||
ToolTip.SetTip(content, BuildKeyTooltip(token, state));
|
||||
@@ -824,7 +773,7 @@ public partial class Settings : UserControl
|
||||
|
||||
if (cap.Token is null)
|
||||
{
|
||||
// Reserved key (Esc, F1-F12, modifiers …): greyed out and not editable.
|
||||
|
||||
content.Children.Add(new Border { Background = KeyFillerBrush });
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
@@ -885,7 +834,6 @@ public partial class Settings : UserControl
|
||||
return head + "\n" + string.Join("\n", state.Tips);
|
||||
}
|
||||
|
||||
// ── key assignment flyout (one combo per modifier combination) ──────────
|
||||
private void ShowKeyFlyout(string token, Control anchor)
|
||||
{
|
||||
CancelCapture();
|
||||
@@ -987,7 +935,6 @@ public partial class Settings : UserControl
|
||||
{
|
||||
KeyBinding? chosen = selectedIndex <= 0 ? null : commands[selectedIndex - 1];
|
||||
|
||||
// Free the slot: unbind any other command currently sitting on it.
|
||||
foreach (var b in KeyboardConfig.Instance.Bindings)
|
||||
{
|
||||
if (!ReferenceEquals(b, chosen) && b.IsAssigned &&
|
||||
@@ -1048,7 +995,6 @@ public partial class Settings : UserControl
|
||||
return sp;
|
||||
}
|
||||
|
||||
// ── shared helpers ─────────────────────────────────────────────────────
|
||||
private sealed class KeyState
|
||||
{
|
||||
public bool Plain;
|
||||
@@ -1106,8 +1052,6 @@ public partial class Settings : UserControl
|
||||
return string.Join(" + ", parts);
|
||||
}
|
||||
|
||||
// Display label for a command: its (capitalised) description, or the raw
|
||||
// command token when the file has no comment for it.
|
||||
private static string CommandLabel(KeyBinding b) =>
|
||||
string.IsNullOrEmpty(b.Description) ? b.Command : Capitalize(b.Description);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ public partial class TextureBaseWindow : Window
|
||||
private readonly VehicleDatabase _db = GameData.Instance.Vehicles;
|
||||
private readonly List<Row> _all = new();
|
||||
|
||||
/// <summary>Texture chosen by double-click (null if the window was just closed).</summary>
|
||||
public VehicleTexture? Picked { get; private set; }
|
||||
|
||||
public TextureBaseWindow()
|
||||
|
||||
Reference in New Issue
Block a user