mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-05 13:24:51 +01:00
Show consist stats, keep empty trainsets and restore last scenery in Scenarios
#20 length/mass/vehicle/track status bar; #17 empty trainsets stay listed; #19 remember last scenery; #16 default starting vehicle; scenery i18n labels, attachments/includes/info tab, weather restore and reload.
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition />
|
<RowDefinition />
|
||||||
<RowDefinition Height="78" />
|
<RowDefinition Height="150" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<!-- Scenery tree (left) -->
|
<!-- Scenery tree (left) -->
|
||||||
@@ -27,6 +27,14 @@
|
|||||||
IsCheckedChanged="ArchivalSwitch_OnChanged"
|
IsCheckedChanged="ArchivalSwitch_OnChanged"
|
||||||
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
|
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
|
||||||
OffContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}" />
|
OffContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}" />
|
||||||
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8" Margin="2,4,0,0">
|
||||||
|
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
||||||
|
Click="ReloadSceneries_OnClick"
|
||||||
|
Content="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
|
||||||
|
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
|
||||||
|
Click="OpenSceneryDir_OnClick"
|
||||||
|
Content="{Binding [OpenSceneryFolder], Source={x:Static starterNg:App.Loc}}" />
|
||||||
|
</StackPanel>
|
||||||
<TreeView Name="sceneryList" Classes="Compact"
|
<TreeView Name="sceneryList" Classes="Compact"
|
||||||
SelectionChanged="SceneryList_OnSelectionChanged" />
|
SelectionChanged="SceneryList_OnSelectionChanged" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
@@ -85,6 +93,10 @@
|
|||||||
VerticalAlignment="Center" Padding="10,4"
|
VerticalAlignment="Center" Padding="10,4"
|
||||||
Click="WeatherTimeNow_OnClick"
|
Click="WeatherTimeNow_OnClick"
|
||||||
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
|
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
|
||||||
|
<Button Name="weatherRestore" Classes="Flat" Cursor="Hand"
|
||||||
|
VerticalAlignment="Center" Padding="10,4"
|
||||||
|
Click="WeatherRestore_OnClick"
|
||||||
|
Content="{Binding [RestoreWeather], Source={x:Static starterNg:App.Loc}}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
|
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
|
||||||
@@ -109,7 +121,7 @@
|
|||||||
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center"
|
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center"
|
||||||
Text="{Binding [Temperature], Source={x:Static starterNg:App.Loc}}" />
|
Text="{Binding [Temperature], Source={x:Static starterNg:App.Loc}}" />
|
||||||
<StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
<StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||||
<Slider Name="weatherTemp" Minimum="-20" Maximum="40" Width="220"
|
<Slider Name="weatherTemp" Minimum="-50" Maximum="50" Width="220"
|
||||||
TickFrequency="1" IsSnapToTickEnabled="True"
|
TickFrequency="1" IsSnapToTickEnabled="True"
|
||||||
ValueChanged="Weather_OnSliderChanged" />
|
ValueChanged="Weather_OnSliderChanged" />
|
||||||
<TextBlock Name="weatherTempLabel" VerticalAlignment="Center" MinWidth="48" />
|
<TextBlock Name="weatherTempLabel" VerticalAlignment="Center" MinWidth="48" />
|
||||||
@@ -150,6 +162,13 @@
|
|||||||
FontSize="12" />
|
FontSize="12" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
<TabItem Name="infoTab" Header="{Binding [TabInfo], Source={x:Static starterNg:App.Loc}}">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBox Name="faultListBox" Margin="4" IsReadOnly="True"
|
||||||
|
AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
FontFamily="Consolas, monospace" FontSize="12" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -169,18 +188,26 @@
|
|||||||
|
|
||||||
<HeaderedContentControl Grid.Row="1" Theme="{StaticResource GroupBox}"
|
<HeaderedContentControl Grid.Row="1" Theme="{StaticResource GroupBox}"
|
||||||
Header="{Binding [SceneryDescription], Source={x:Static starterNg:App.Loc}}">
|
Header="{Binding [SceneryDescription], Source={x:Static starterNg:App.Loc}}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<DockPanel LastChildFill="True">
|
||||||
<TextBlock TextWrapping="Wrap" Name="sceneryDescription" />
|
<StackPanel Name="attachmentsPanel" DockPanel.Dock="Bottom" Spacing="2" Margin="0,6,0,0" />
|
||||||
</ScrollViewer>
|
<StackPanel Name="includesPanel" DockPanel.Dock="Bottom" Spacing="2" Margin="0,6,0,0" />
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBlock TextWrapping="Wrap" Name="sceneryDescription" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Consist preview (full-width bottom, horizontally scrollable) -->
|
<!-- Consist preview (full-width bottom, horizontally scrollable) -->
|
||||||
<HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}"
|
<HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}"
|
||||||
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
|
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
<DockPanel LastChildFill="True">
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Name="consistStack" />
|
<TextBlock Name="trainStats" DockPanel.Dock="Top" FontSize="12"
|
||||||
</ScrollViewer>
|
Opacity="0.85" Margin="0,0,0,6" TextTrimming="CharacterEllipsis" />
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Name="consistStack" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
</HeaderedContentControl>
|
</HeaderedContentControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ using System.Text.RegularExpressions;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Layout;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using StarterNG.Classes;
|
using StarterNG.Classes;
|
||||||
|
using StarterNG.Domain;
|
||||||
|
|
||||||
namespace StarterNG.Views;
|
namespace StarterNG.Views;
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ public partial class Scenarios : UserControl
|
|||||||
_loadingFilters = false;
|
_loadingFilters = false;
|
||||||
|
|
||||||
BuildSceneryTree();
|
BuildSceneryTree();
|
||||||
|
RestoreLastScenery();
|
||||||
|
|
||||||
// refresh the consist preview when the view is shown again (e.g. after
|
// refresh the consist preview when the view is shown again (e.g. after
|
||||||
// editing the consist in the depot). Switching tabs only toggles IsVisible
|
// editing the consist in the depot). Switching tabs only toggles IsVisible
|
||||||
@@ -86,11 +89,15 @@ public partial class Scenarios : UserControl
|
|||||||
if (scenery.Archival && !showArchival)
|
if (scenery.Archival && !showArchival)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
bool expand = StarterNG.Classes.Settings.Instance.AutoExpandSceneryTree;
|
||||||
|
SceneryI18n.LoadFor(scenery, App.Loc.CurrentLangCode);
|
||||||
|
string label = scenery.DisplayName;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(scenery.Group))
|
if (string.IsNullOrEmpty(scenery.Group))
|
||||||
{
|
{
|
||||||
topLevel.Add(new TreeViewItem
|
topLevel.Add(new TreeViewItem
|
||||||
{
|
{
|
||||||
Header = Path.GetFileNameWithoutExtension(scenery.Path),
|
Header = label,
|
||||||
Tag = i
|
Tag = i
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@@ -98,14 +105,18 @@ public partial class Scenarios : UserControl
|
|||||||
|
|
||||||
if (!groupNodes.TryGetValue(scenery.Group, out var groupNode))
|
if (!groupNodes.TryGetValue(scenery.Group, out var groupNode))
|
||||||
{
|
{
|
||||||
groupNode = new TreeViewItem { Header = scenery.Group, IsExpanded = false };
|
groupNode = new TreeViewItem
|
||||||
|
{
|
||||||
|
Header = SceneryI18n.T(scenery.Group),
|
||||||
|
IsExpanded = expand
|
||||||
|
};
|
||||||
groupNodes[scenery.Group] = groupNode;
|
groupNodes[scenery.Group] = groupNode;
|
||||||
topLevel.Add(groupNode);
|
topLevel.Add(groupNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
groupNode.Items.Add(new TreeViewItem
|
groupNode.Items.Add(new TreeViewItem
|
||||||
{
|
{
|
||||||
Header = Path.GetFileNameWithoutExtension(scenery.Path),
|
Header = label,
|
||||||
Tag = i
|
Tag = i
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,9 +135,102 @@ public partial class Scenarios : UserControl
|
|||||||
sceneryList.Items.Add(item);
|
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;
|
||||||
|
string want = Path.GetFileNameWithoutExtension(last);
|
||||||
|
|
||||||
|
TreeViewItem? MatchLeaf(TreeViewItem node)
|
||||||
|
{
|
||||||
|
if (node.Tag is not int idx || idx < 0 || idx >= Sceneries.Count)
|
||||||
|
return null;
|
||||||
|
var scn = Sceneries[idx];
|
||||||
|
if (string.IsNullOrWhiteSpace(want)) return null;
|
||||||
|
if (string.Equals(scn.DisplayName, want, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(Path.GetFileNameWithoutExtension(scn.Path), want, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(node.Header as string, want, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return node;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeViewItem? firstLeaf = null;
|
||||||
|
foreach (var obj in sceneryList.Items)
|
||||||
|
{
|
||||||
|
if (obj is not TreeViewItem node)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (node.Tag is int)
|
||||||
|
{
|
||||||
|
firstLeaf ??= node;
|
||||||
|
if (MatchLeaf(node) is { } hit)
|
||||||
|
{
|
||||||
|
ExpandAncestors(hit);
|
||||||
|
sceneryList.SelectedItem = hit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var childObj in node.Items)
|
||||||
|
{
|
||||||
|
if (childObj is not TreeViewItem { Tag: int } child)
|
||||||
|
continue;
|
||||||
|
firstLeaf ??= child;
|
||||||
|
if (MatchLeaf(child) is { } hit)
|
||||||
|
{
|
||||||
|
node.IsExpanded = true;
|
||||||
|
sceneryList.SelectedItem = hit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstLeaf != null)
|
||||||
|
{
|
||||||
|
if (firstLeaf.Parent is TreeViewItem parent)
|
||||||
|
parent.IsExpanded = true;
|
||||||
|
sceneryList.SelectedItem = firstLeaf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExpandAncestors(TreeViewItem node)
|
||||||
|
{
|
||||||
|
var parent = node.Parent;
|
||||||
|
while (parent != null)
|
||||||
|
{
|
||||||
|
if (parent is TreeViewItem tvi)
|
||||||
|
tvi.IsExpanded = true;
|
||||||
|
parent = parent.Parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
StarterNG.Classes.Settings.Instance.LastScenery = name;
|
||||||
|
StarterNG.Classes.Settings.Instance.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrainsetListLabel(Trainset trainset) =>
|
||||||
|
TrainsetDisplay.CompositionText(trainset)
|
||||||
|
?? string.Format(App.Loc["EmptyTrainset"], trainset.Track ?? "");
|
||||||
|
|
||||||
|
private static Control TrainsetListContent(Trainset trainset) =>
|
||||||
|
new Avalonia.Controls.TextBlock
|
||||||
|
{
|
||||||
|
Text = TrainsetListLabel(trainset),
|
||||||
|
TextTrimming = Avalonia.Media.TextTrimming.CharacterEllipsis,
|
||||||
|
TextWrapping = Avalonia.Media.TextWrapping.NoWrap
|
||||||
|
};
|
||||||
|
|
||||||
// Fills the Vehicles list for a scenery, honouring the two filters:
|
// Fills the Vehicles list for a scenery, honouring the two filters:
|
||||||
// - "AI vehicles" off hides consists whose //$o description starts with '-';
|
// - "AI vehicles" off hides consists whose //$o description starts with '-';
|
||||||
// - "Drivable only" on hides decoration consists (the //$decor flag).
|
// - "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)
|
private void PopulateVehicleList(Scenery scn)
|
||||||
{
|
{
|
||||||
vehicleList.Items.Clear();
|
vehicleList.Items.Clear();
|
||||||
@@ -137,15 +241,13 @@ public partial class Scenarios : UserControl
|
|||||||
for (int i = 0; i < scn.Trainsets.Count; i++)
|
for (int i = 0; i < scn.Trainsets.Count; i++)
|
||||||
{
|
{
|
||||||
var trainset = scn.Trainsets[i];
|
var trainset = scn.Trainsets[i];
|
||||||
string trainsetName = string.Join(" + ", trainset.Vehicles.Select(dyn => dyn.Name));
|
|
||||||
if (string.IsNullOrWhiteSpace(trainsetName)) continue; // skip empty
|
|
||||||
|
|
||||||
if (IsAiTrainset(trainset) && !showAi) continue;
|
if (IsAiTrainset(trainset) && !showAi) continue;
|
||||||
if (trainset.Decor && drivableOnly) continue;
|
if (trainset.Decor && drivableOnly) continue;
|
||||||
|
|
||||||
var listItem = new ListBoxItem
|
var listItem = new ListBoxItem
|
||||||
{
|
{
|
||||||
Content = trainsetName,
|
Content = TrainsetListContent(trainset),
|
||||||
Tag = i
|
Tag = i
|
||||||
};
|
};
|
||||||
// right-click: save/load to the warehouse + clipboard copy/paste
|
// right-click: save/load to the warehouse + clipboard copy/paste
|
||||||
@@ -188,6 +290,7 @@ public partial class Scenarios : UserControl
|
|||||||
StarterNG.Classes.Settings.Instance.Save();
|
StarterNG.Classes.Settings.Instance.Save();
|
||||||
|
|
||||||
BuildSceneryTree();
|
BuildSceneryTree();
|
||||||
|
RestoreLastScenery();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replaces a consist's vehicles (from a loaded warehouse preset or a pasted
|
// Replaces a consist's vehicles (from a loaded warehouse preset or a pasted
|
||||||
@@ -195,7 +298,7 @@ public partial class Scenarios : UserControl
|
|||||||
private void ApplyConsist(ListBoxItem item, Trainset trainset, List<Dynamic> vehicles)
|
private void ApplyConsist(ListBoxItem item, Trainset trainset, List<Dynamic> vehicles)
|
||||||
{
|
{
|
||||||
trainset.Vehicles = vehicles;
|
trainset.Vehicles = vehicles;
|
||||||
item.Content = string.Join(" + ", trainset.Vehicles.Select(d => d.Name));
|
item.Content = TrainsetListContent(trainset);
|
||||||
if (ReferenceEquals(AppState.Instance.CurrentTrainset, trainset))
|
if (ReferenceEquals(AppState.Instance.CurrentTrainset, trainset))
|
||||||
ShowConsist(trainset);
|
ShowConsist(trainset);
|
||||||
}
|
}
|
||||||
@@ -212,7 +315,7 @@ public partial class Scenarios : UserControl
|
|||||||
foreach (var obj in vehicleList.Items)
|
foreach (var obj in vehicleList.Items)
|
||||||
{
|
{
|
||||||
if (obj is ListBoxItem { Tag: int vi } lbi && vi < scn.Trainsets.Count)
|
if (obj is ListBoxItem { Tag: int vi } lbi && vi < scn.Trainsets.Count)
|
||||||
lbi.Content = string.Join(" + ", scn.Trainsets[vi].Vehicles.Select(d => d.Name));
|
lbi.Content = TrainsetListContent(scn.Trainsets[vi]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +341,7 @@ public partial class Scenarios : UserControl
|
|||||||
// scenery-level info (distinct from the per-consist scenario description)
|
// scenery-level info (distinct from the per-consist scenario description)
|
||||||
ShowSceneryInfo(selectedScn);
|
ShowSceneryInfo(selectedScn);
|
||||||
ShowWeather(selectedScn);
|
ShowWeather(selectedScn);
|
||||||
|
PersistLastScenery(selectedScn);
|
||||||
|
|
||||||
// no consist chosen yet for this scenery
|
// no consist chosen yet for this scenery
|
||||||
missionDescription.Text = "";
|
missionDescription.Text = "";
|
||||||
@@ -250,7 +354,8 @@ public partial class Scenarios : UserControl
|
|||||||
// Shows the selected scenery's description (//$d) and 1:1 image (//$i).
|
// Shows the selected scenery's description (//$d) and 1:1 image (//$i).
|
||||||
private void ShowSceneryInfo(Scenery scenery)
|
private void ShowSceneryInfo(Scenery scenery)
|
||||||
{
|
{
|
||||||
sceneryDescription.Text = scenery.Description;
|
SceneryI18n.LoadFor(scenery, App.Loc.CurrentLangCode);
|
||||||
|
sceneryDescription.Text = scenery.LocalizedDescription;
|
||||||
|
|
||||||
string? imagePath = scenery.ImagePath;
|
string? imagePath = scenery.ImagePath;
|
||||||
if (imagePath is not null)
|
if (imagePath is not null)
|
||||||
@@ -261,13 +366,154 @@ public partial class Scenarios : UserControl
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
sceneryImage.Source = null; // unreadable / unsupported image
|
sceneryImage.Source = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
sceneryImage.Source = null;
|
sceneryImage.Source = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopulateAttachments(scenery);
|
||||||
|
PopulateIncludes(scenery);
|
||||||
|
PopulateFaults(scenery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateFaults(Scenery scenery)
|
||||||
|
{
|
||||||
|
var lines = new List<string>();
|
||||||
|
lines.AddRange(scenery.Faults);
|
||||||
|
|
||||||
|
var db = GameData.Instance.Vehicles;
|
||||||
|
foreach (var ts in scenery.Trainsets)
|
||||||
|
{
|
||||||
|
if (!ts.Parsed)
|
||||||
|
lines.Add($"# unparsed trainset: {ts.Name}");
|
||||||
|
foreach (var v in ts.Vehicles)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(v.SkinFile)) continue;
|
||||||
|
if (db.TextureForSkin(v.SkinFile) is null)
|
||||||
|
lines.Add($"# unknown texture: {v.Name} ({v.SkinFile})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var v in scenery.LooseVehicles)
|
||||||
|
{
|
||||||
|
if (db.TextureForSkin(v.SkinFile) is null)
|
||||||
|
lines.Add($"# unknown loose texture: {v.Name} ({v.SkinFile})");
|
||||||
|
}
|
||||||
|
|
||||||
|
faultListBox.Text = string.Join(Environment.NewLine, lines);
|
||||||
|
infoTab.IsVisible = lines.Count > 0 || scenery.Includes.Any(i => i.Kind != 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateAttachments(Scenery scenery)
|
||||||
|
{
|
||||||
|
attachmentsPanel.Children.Clear();
|
||||||
|
foreach (var att in scenery.Attachments)
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = att.Label,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
HorizontalContentAlignment = HorizontalAlignment.Left,
|
||||||
|
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand),
|
||||||
|
Tag = att.FilePath
|
||||||
|
};
|
||||||
|
btn.Classes.Add("Flat");
|
||||||
|
ToolTip.SetTip(btn, att.FilePath);
|
||||||
|
btn.Click += (_, _) => OpenAttachment(att.FilePath);
|
||||||
|
attachmentsPanel.Children.Add(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateIncludes(Scenery scenery)
|
||||||
|
{
|
||||||
|
includesPanel.Children.Clear();
|
||||||
|
var opts = scenery.Includes.Where(i => i.Kind != 2).ToList();
|
||||||
|
if (opts.Count == 0) return;
|
||||||
|
|
||||||
|
includesPanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = App.Loc["OptionalIncludes"],
|
||||||
|
FontWeight = Avalonia.Media.FontWeight.Bold,
|
||||||
|
FontSize = 12,
|
||||||
|
Margin = new Thickness(0, 0, 0, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var inc in opts)
|
||||||
|
{
|
||||||
|
var check = new CheckBox
|
||||||
|
{
|
||||||
|
Content = $"{inc.Desc} ({inc.FilePath})",
|
||||||
|
IsChecked = inc.Selected,
|
||||||
|
FontSize = 11,
|
||||||
|
Tag = inc
|
||||||
|
};
|
||||||
|
check.IsCheckedChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (check.Tag is SceneryInclude s)
|
||||||
|
s.Selected = check.IsChecked == true;
|
||||||
|
};
|
||||||
|
includesPanel.Children.Add(check);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenAttachment(string relativePath)
|
||||||
|
{
|
||||||
|
string full = Path.GetFullPath(relativePath);
|
||||||
|
if (!File.Exists(full))
|
||||||
|
full = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));
|
||||||
|
if (!File.Exists(full))
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(full) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch { /* no handler */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Rebuild tree labels after UI / scenery i18n language change.</summary>
|
||||||
|
public void RebuildAfterLanguageChange()
|
||||||
|
{
|
||||||
|
string? selected = AppState.Instance.CurrentScenery?.Path;
|
||||||
|
BuildSceneryTree();
|
||||||
|
if (!string.IsNullOrEmpty(selected))
|
||||||
|
{
|
||||||
|
StarterNG.Classes.Settings.Instance.LastScenery =
|
||||||
|
Path.GetFileNameWithoutExtension(selected);
|
||||||
|
RestoreLastScenery();
|
||||||
|
}
|
||||||
|
if (AppState.Instance.CurrentScenery != null)
|
||||||
|
ShowSceneryInfo(AppState.Instance.CurrentScenery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReloadSceneries_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
string? selected = AppState.Instance.CurrentScenery?.Path;
|
||||||
|
GameData.Instance.ReloadSceneries();
|
||||||
|
Sceneries = GameData.Instance.Sceneries;
|
||||||
|
BuildSceneryTree();
|
||||||
|
if (!string.IsNullOrEmpty(selected))
|
||||||
|
{
|
||||||
|
StarterNG.Classes.Settings.Instance.LastScenery =
|
||||||
|
Path.GetFileNameWithoutExtension(selected);
|
||||||
|
}
|
||||||
|
RestoreLastScenery();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenSceneryDir_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var scn = AppState.Instance.CurrentScenery;
|
||||||
|
string dir = scn != null
|
||||||
|
? (Path.GetDirectoryName(Path.GetFullPath(scn.Path)) ?? "scenery")
|
||||||
|
: Path.GetFullPath("scenery");
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo(dir) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
@@ -287,6 +533,7 @@ public partial class Scenarios : UserControl
|
|||||||
// share the selection so the depot edits this consist in place
|
// share the selection so the depot edits this consist in place
|
||||||
AppState.Instance.CurrentScenery = selectedScn;
|
AppState.Instance.CurrentScenery = selectedScn;
|
||||||
AppState.Instance.CurrentTrainset = selectedTrainset;
|
AppState.Instance.CurrentTrainset = selectedTrainset;
|
||||||
|
AppState.Instance.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(selectedTrainset);
|
||||||
|
|
||||||
ShowConsist(selectedTrainset);
|
ShowConsist(selectedTrainset);
|
||||||
ShowTimetable(selectedScn, selectedTrainset);
|
ShowTimetable(selectedScn, selectedTrainset);
|
||||||
@@ -302,7 +549,7 @@ public partial class Scenarios : UserControl
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
weatherForm.IsEnabled = true;
|
weatherForm.IsEnabled = true;
|
||||||
weatherTime.SelectedTime = ParseTime(scenery.WeatherTime);
|
weatherTime.SelectedTime = ParseTime(scenery.ScenarioTimeOverride);
|
||||||
weatherDay.Value = scenery.Day;
|
weatherDay.Value = scenery.Day;
|
||||||
weatherTemp.Value = scenery.Temperature;
|
weatherTemp.Value = scenery.Temperature;
|
||||||
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
|
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
|
||||||
@@ -326,7 +573,7 @@ public partial class Scenarios : UserControl
|
|||||||
|
|
||||||
var scn = _currentScenery;
|
var scn = _currentScenery;
|
||||||
if (weatherTime.SelectedTime is { } t)
|
if (weatherTime.SelectedTime is { } t)
|
||||||
scn.WeatherTime = $"{t.Hours:D2}:{t.Minutes:D2}";
|
scn.ScenarioTimeOverride = $"{t.Hours:D2}:{t.Minutes:D2}";
|
||||||
// In "Today" mode the day field is only a preview, so keep the stored day
|
// In "Today" mode the day field is only a preview, so keep the stored day
|
||||||
// at 0 (the scenery then follows the live date).
|
// at 0 (the scenery then follows the live date).
|
||||||
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
|
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
|
||||||
@@ -368,6 +615,14 @@ public partial class Scenarios : UserControl
|
|||||||
weatherTime.SelectedTime = new TimeSpan(now.Hour, now.Minute, 0);
|
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;
|
||||||
|
_currentScenery.RestoreWeather();
|
||||||
|
ShowWeather(_currentScenery);
|
||||||
|
}
|
||||||
|
|
||||||
// Parses a stored "hh:mm" string into a TimeSpan, or null when it is missing
|
// Parses a stored "hh:mm" string into a TimeSpan, or null when it is missing
|
||||||
// or malformed (so the picker simply shows no selection).
|
// or malformed (so the picker simply shows no selection).
|
||||||
private static TimeSpan? ParseTime(string? text)
|
private static TimeSpan? ParseTime(string? text)
|
||||||
@@ -484,15 +739,38 @@ public partial class Scenarios : UserControl
|
|||||||
if (path is null)
|
if (path is null)
|
||||||
continue; // fallback: no mini
|
continue; // fallback: no mini
|
||||||
|
|
||||||
|
int thumbH = StarterNG.Classes.Settings.Instance.LargeThumbnails ? 64 : 32;
|
||||||
consistStack.Children.Add(new Image
|
consistStack.Children.Add(new Image
|
||||||
{
|
{
|
||||||
Source = new Bitmap(path),
|
Source = new Bitmap(path),
|
||||||
Height = 32,
|
Height = thumbH,
|
||||||
Stretch = Avalonia.Media.Stretch.Uniform,
|
Stretch = Avalonia.Media.Stretch.Uniform,
|
||||||
Margin = new Thickness(0)
|
Margin = new Thickness(0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
missionDescription.Text = trainset.Description;
|
string desc = trainset.Description ?? "";
|
||||||
|
if (desc.TrimStart().StartsWith('-'))
|
||||||
|
desc = desc.TrimStart()[1..].TrimStart();
|
||||||
|
missionDescription.Text = SceneryI18n.T(desc);
|
||||||
|
UpdateTrainStats(trainset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTrainStats(Trainset? trainset)
|
||||||
|
{
|
||||||
|
if (trainset is null)
|
||||||
|
{
|
||||||
|
trainStats.Text = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = GameData.Instance.Vehicles;
|
||||||
|
trainStats.Text = TrainsetDisplay.FormatStats(
|
||||||
|
trainset,
|
||||||
|
car => Physics.For(car.DataFolder, db.TextureForSkin(car.SkinFile)?.Model)
|
||||||
|
?? Physics.For(car.DataFolder, car.MmdFile)
|
||||||
|
?? Physics.For(car.DataFolder, car.SkinFile),
|
||||||
|
App.Loc["Length"], App.Loc["Mass"], App.Loc["VehicleCount"], App.Loc["Track"],
|
||||||
|
car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When returning to this view, refresh the preview of the selected consist
|
// When returning to this view, refresh the preview of the selected consist
|
||||||
|
|||||||
Reference in New Issue
Block a user