mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-15 20:44:12 +01:00
Compare commits
13 Commits
feature/bl
...
fix/reshad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2dbf9eac | ||
|
|
91c496ab48 | ||
|
|
744720d922 | ||
|
|
b5ba67430f | ||
|
|
c261d4b0ab | ||
|
|
3aa6a256a5 | ||
|
|
95c510b3ac | ||
|
|
5af567be7f | ||
|
|
b201b6f7ab | ||
|
|
3a19fd5da2 | ||
|
|
36df14abee | ||
|
|
da5a1efea1 | ||
|
|
e5330d91a6 |
13
.config/dotnet-tools.json
Normal file
13
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-sonarscanner": {
|
||||
"version": "12.1.0",
|
||||
"commands": [
|
||||
"dotnet-sonarscanner"
|
||||
],
|
||||
"rollForward": true
|
||||
}
|
||||
}
|
||||
}
|
||||
114
.github/workflows/sonar.yml
vendored
Normal file
114
.github/workflows/sonar.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
# .github/workflows/sonar.yml
|
||||
# Has SONAR_TOKEN, so it only ever builds code that is already trusted: pushes to
|
||||
# main and pull requests opened from a branch of this repository.
|
||||
#
|
||||
# Unlike the C++ analysis of maszyna, this one cannot be split into an untrusted
|
||||
# build and a trusted scan. The C# analyzers run inside the compiler, and the
|
||||
# scanner's begin step, which needs the token, has to run before the build on the
|
||||
# same machine. A fork's build could leave a process behind to read the token from
|
||||
# the end step, so fork pull requests are not analysed here. GitHub withholds the
|
||||
# secret from them (and from Dependabot) anyway.
|
||||
#
|
||||
# The scanner is pinned in .config/dotnet-tools.json, where Dependabot updates it.
|
||||
# SonarScanner for .NET takes its settings on the command line only; it refuses
|
||||
# to run next to a sonar-project.properties, so there is none.
|
||||
|
||||
name: SonarQube Cloud
|
||||
|
||||
on:
|
||||
# Main branch only: analysing other branches is a paid feature, pull requests
|
||||
# are not.
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- staging
|
||||
types: [ opened, reopened, synchronize ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: sonar-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_NOLOGO: true
|
||||
SOLUTION_PATH: StarterNG.sln
|
||||
TEST_PROJECT_PATH: StarterNG.Tests/StarterNG.Tests.csproj
|
||||
|
||||
jobs:
|
||||
sonar:
|
||||
name: Analyze
|
||||
if: >
|
||||
github.event_name != 'pull_request' ||
|
||||
(github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.actor != 'dependabot[bot]')
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
# Full history, so new code and blame are told apart correctly.
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup .NET 9
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
|
||||
# sonar.qualitygate.wait=true blocks the end step until the server has
|
||||
# processed the report, so the job's result is the quality gate's.
|
||||
- name: Begin analysis
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: >
|
||||
dotnet tool run dotnet-sonarscanner begin
|
||||
/k:"MaSzyna-EU07_StarterNG"
|
||||
/o:"maszyna-eu07"
|
||||
/n:"StarterNG"
|
||||
/d:sonar.host.url="https://sonarcloud.io"
|
||||
/d:sonar.token="$SONAR_TOKEN"
|
||||
/d:sonar.scm.revision="${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
/d:sonar.cs.opencover.reportsPaths="TestResults/**/coverage.opencover.xml"
|
||||
/d:sonar.cs.vstest.reportsPaths="TestResults/*.trx"
|
||||
/d:sonar.qualitygate.wait=true
|
||||
|
||||
# A full, non-incremental build: the analyzers only see what is compiled.
|
||||
- name: Build
|
||||
run: >
|
||||
dotnet build "${{ env.SOLUTION_PATH }}"
|
||||
-c Release
|
||||
--no-incremental
|
||||
|
||||
# Coverlet is already referenced by the test project; OpenCover is the
|
||||
# format the C# analyzer reads.
|
||||
- name: Test with coverage
|
||||
run: >
|
||||
dotnet test "${{ env.TEST_PROJECT_PATH }}"
|
||||
-c Release
|
||||
--no-build
|
||||
--results-directory TestResults
|
||||
--logger trx
|
||||
--collect "XPlat Code Coverage"
|
||||
--
|
||||
DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
|
||||
|
||||
- name: End analysis
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: >
|
||||
dotnet tool run dotnet-sonarscanner end
|
||||
/d:sonar.token="$SONAR_TOKEN"
|
||||
@@ -65,6 +65,19 @@ public class PhysicsAndAssetsTests
|
||||
Assert.Equal(125, physics.VMax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_dot_in_the_model_name_is_part_of_the_name()
|
||||
{
|
||||
// As in the game: cd/9-107_v1 holds 9-107.13.fiz, 9-107.6.fiz and more.
|
||||
var installation = new TestInstallation(new InMemoryFileSystem()
|
||||
.WithFile(Fiz("cd", "9-107_v1", "9-107.13"), "Load: LoadAccepted=coal MaxLoad=58")
|
||||
.WithFile(Fiz("cd", "9-107_v1", "9-107.6"), "Load: LoadAccepted=coal MaxLoad=55"));
|
||||
|
||||
Assert.Equal(58, installation.Physics.For("cd/9-107_v1/", "9-107.13")!.MaxLoad);
|
||||
Assert.Equal(55, installation.Physics.For("cd/9-107_v1/", "9-107.6")!.MaxLoad);
|
||||
Assert.Equal(58, installation.Physics.For("cd/9-107_v1/", "9-107.13.e3d")!.MaxLoad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_dumb_variant_stands_in_when_the_model_has_no_fiz_of_its_own()
|
||||
{
|
||||
|
||||
45
StarterNG.Tests/SoftwareRenderTests.cs
Normal file
45
StarterNG.Tests/SoftwareRenderTests.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Avalonia;
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class SoftwareRenderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("--software-render")]
|
||||
[InlineData("--Software-Render")]
|
||||
public void The_switch_asks_for_it_in_any_case(string arg)
|
||||
{
|
||||
Assert.True(SoftwareRender.AskedIn(new[] { "--other", arg }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Without_the_switch_the_graphics_card_draws()
|
||||
{
|
||||
Assert.False(SoftwareRender.AskedIn(Array.Empty<string>()));
|
||||
Assert.False(SoftwareRender.AskedIn(new[] { "--software" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Windows_draws_on_the_processor_into_a_plain_window_surface()
|
||||
{
|
||||
var options = SoftwareRender.Win32Options;
|
||||
|
||||
Assert.Equal(new[] { Win32RenderingMode.Software }, options.RenderingMode);
|
||||
Assert.Equal(new[] { Win32CompositionMode.RedirectionSurface }, options.CompositionMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void X11_draws_on_the_processor()
|
||||
{
|
||||
Assert.Equal(new[] { X11RenderingMode.Software }, SoftwareRender.X11Options.RenderingMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Both_platforms_options_go_to_the_same_builder()
|
||||
{
|
||||
var builder = AppBuilder.Configure<Avalonia.Application>();
|
||||
|
||||
Assert.Same(builder, SoftwareRender.ApplyTo(builder));
|
||||
}
|
||||
}
|
||||
98
StarterNG.Tests/SystemGraphicsLibrariesTests.cs
Normal file
98
StarterNG.Tests/SystemGraphicsLibrariesTests.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Tests;
|
||||
|
||||
public class SystemGraphicsLibrariesTests
|
||||
{
|
||||
private const string System32 = @"C:\Windows\System32";
|
||||
private const string Game = @"E:\SteamLibrary\steamapps\common\MaSzyna";
|
||||
|
||||
private static readonly IntPtr Dxgi = new(0x1000);
|
||||
private static readonly IntPtr D3d11 = new(0x2000);
|
||||
|
||||
private static SystemGraphicsLibraries Take(Func<string, bool> beside, Dictionary<string, IntPtr> system)
|
||||
{
|
||||
return SystemGraphicsLibraries.Take(System32, Game, beside, Load);
|
||||
|
||||
bool Load(string path, out IntPtr handle)
|
||||
{
|
||||
handle = system.GetValueOrDefault(Path.GetFileName(path));
|
||||
return handle != IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemGraphicsLibraries WithDxgiAndD3d11(Func<string, bool> beside) =>
|
||||
Take(beside, new Dictionary<string, IntPtr> { ["dxgi.dll"] = Dxgi, ["d3d11.dll"] = D3d11 });
|
||||
|
||||
[Fact]
|
||||
public void An_import_by_name_gets_the_system_library_with_or_without_its_extension()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
|
||||
Assert.Equal(Dxgi, taken.Resolve("dxgi"));
|
||||
Assert.Equal(Dxgi, taken.Resolve("dxgi.dll"));
|
||||
Assert.Equal(D3d11, taken.Resolve("D3D11.DLL"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Other_libraries_and_ones_the_system_lacks_are_left_to_the_usual_search()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
|
||||
Assert.Equal(IntPtr.Zero, taken.Resolve("user32.dll"));
|
||||
Assert.Equal(IntPtr.Zero, taken.Resolve("opengl32"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void They_are_loaded_from_the_system_folder_dxgi_first()
|
||||
{
|
||||
var asked = new List<string>();
|
||||
SystemGraphicsLibraries.Take(System32, Game, _ => false, Load);
|
||||
|
||||
Assert.Equal(SystemGraphicsLibraries.Names.Select(n => Path.Combine(System32, n + ".dll")), asked);
|
||||
Assert.Equal("dxgi", SystemGraphicsLibraries.Names[0]);
|
||||
|
||||
bool Load(string path, out IntPtr handle)
|
||||
{
|
||||
asked.Add(path);
|
||||
handle = IntPtr.Zero;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_wrapper_beside_the_starter_is_noted()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(path => path == Path.Combine(Game, "dxgi.dll"));
|
||||
|
||||
Assert.Equal(new[] { "dxgi.dll" }, taken.Shadowed);
|
||||
Assert.Contains("dxgi.dll", taken.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void With_nothing_beside_the_starter_there_is_nothing_to_note()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
|
||||
Assert.Empty(taken.Shadowed);
|
||||
Assert.Null(taken.Note);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Only_windows_has_system_libraries_to_take()
|
||||
{
|
||||
var taken = SystemGraphicsLibraries.TakeOnWindows();
|
||||
|
||||
Assert.Equal(OperatingSystem.IsWindows(), taken is not null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_assembly_with_a_resolver_of_its_own_keeps_it()
|
||||
{
|
||||
var taken = WithDxgiAndD3d11(_ => false);
|
||||
var assembly = typeof(SystemGraphicsLibrariesTests).Assembly;
|
||||
|
||||
Assert.True(taken.ServeTo(assembly));
|
||||
Assert.False(taken.ServeTo(assembly));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,6 @@ public static class AppInfo
|
||||
#if DEBUG
|
||||
text += " (DEBUG)";
|
||||
#endif
|
||||
return text + " public beta";
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
32
StarterNG/Infrastructure/SoftwareRender.cs
Normal file
32
StarterNG/Infrastructure/SoftwareRender.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Drawn by the processor instead of the graphics card, for a machine on which the
|
||||
/// window comes up empty. Each platform reads only its own options, so both are given.
|
||||
/// </summary>
|
||||
public static class SoftwareRender
|
||||
{
|
||||
public const string Switch = "--software-render";
|
||||
|
||||
public static bool AskedIn(IEnumerable<string> args) =>
|
||||
args.Any(a => string.Equals(a, Switch, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static Win32PlatformOptions Win32Options => new()
|
||||
{
|
||||
RenderingMode = new[] { Win32RenderingMode.Software },
|
||||
CompositionMode = new[] { Win32CompositionMode.RedirectionSurface }
|
||||
};
|
||||
|
||||
public static X11PlatformOptions X11Options => new()
|
||||
{
|
||||
RenderingMode = new[] { X11RenderingMode.Software }
|
||||
};
|
||||
|
||||
public static AppBuilder ApplyTo(AppBuilder builder) =>
|
||||
builder.With(Win32Options).With(X11Options);
|
||||
}
|
||||
90
StarterNG/Infrastructure/SystemGraphicsLibraries.cs
Normal file
90
StarterNG/Infrastructure/SystemGraphicsLibraries.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace StarterNG.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// The starter sits in the simulator's folder, and so do the graphics wrappers a player
|
||||
/// puts there for the simulator - ReShade and the like, named after the system library
|
||||
/// they stand in for. Windows looks in the program's own folder first, so the starter's
|
||||
/// window drew through them too, and with some of them it drew nothing at all.
|
||||
/// Loaded from System32 by full path before anything asks for them, these names are
|
||||
/// taken: a native library that asks by name gets the system one. .NET looks beside the
|
||||
/// program by full path before it asks by name, so the window code's own imports are
|
||||
/// handed the same libraries. The simulator is a process of its own and still finds its
|
||||
/// wrappers.
|
||||
/// Elsewhere there is nothing of the kind to keep out. Under Wine or Proton the system
|
||||
/// folder is the prefix's, so what is taken there is what would have been taken anyway.
|
||||
/// </summary>
|
||||
public sealed class SystemGraphicsLibraries
|
||||
{
|
||||
public delegate bool Loader(string path, out IntPtr handle);
|
||||
|
||||
// dxgi ahead of d3d11, which asks for it.
|
||||
public static readonly IReadOnlyList<string> Names = new[] { "dxgi", "d3d11", "d3d9", "opengl32" };
|
||||
|
||||
private readonly Dictionary<string, IntPtr> _loaded = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<string> _shadowed = new();
|
||||
|
||||
private SystemGraphicsLibraries()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>The files of these names that lay beside the starter.</summary>
|
||||
public IReadOnlyList<string> Shadowed => _shadowed;
|
||||
|
||||
/// <summary>What goes to the log about them, or null when none lay there.</summary>
|
||||
public string? Note => _shadowed.Count > 0
|
||||
? $"Graphics libraries beside the starter, left to the simulator: {string.Join(", ", _shadowed)}"
|
||||
: null;
|
||||
|
||||
/// <summary>The libraries of this Windows, or null on any other system.</summary>
|
||||
public static SystemGraphicsLibraries? TakeOnWindows() =>
|
||||
OperatingSystem.IsWindows()
|
||||
? Take(Environment.SystemDirectory, AppContext.BaseDirectory, File.Exists, NativeLibrary.TryLoad)
|
||||
: null;
|
||||
|
||||
public static SystemGraphicsLibraries Take(string systemDirectory, string ownDirectory,
|
||||
Func<string, bool> exists, Loader load)
|
||||
{
|
||||
var taken = new SystemGraphicsLibraries();
|
||||
foreach (string name in Names)
|
||||
{
|
||||
string file = name + ".dll";
|
||||
if (exists(Path.Combine(ownDirectory, file)))
|
||||
taken._shadowed.Add(file);
|
||||
if (load(Path.Combine(systemDirectory, file), out IntPtr handle))
|
||||
taken._loaded[name] = handle;
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
/// <summary>The system library for an import of one of these names; zero leaves it to the usual search.</summary>
|
||||
public IntPtr Resolve(string libraryName)
|
||||
{
|
||||
string name = libraryName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|
||||
? libraryName[..^4]
|
||||
: libraryName;
|
||||
return _loaded.TryGetValue(name, out IntPtr handle) ? handle : IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The assembly's imports of these names get the system libraries. False when it
|
||||
/// has a resolver of its own already; that one decides.
|
||||
/// </summary>
|
||||
public bool ServeTo(Assembly importer)
|
||||
{
|
||||
try
|
||||
{
|
||||
NativeLibrary.SetDllImportResolver(importer, (name, _, _) => Resolve(name));
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
if (string.IsNullOrEmpty(model))
|
||||
return null;
|
||||
|
||||
string name = Path.GetFileNameWithoutExtension(model);
|
||||
string name = Path.GetFileName(model);
|
||||
if (name.Length == 0)
|
||||
return null;
|
||||
|
||||
@@ -60,7 +60,10 @@ public sealed class FizPhysicsRepository : IPhysicsRepository
|
||||
if (_cache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
|
||||
var physics = Read(folder, name);
|
||||
// The name as written first: in 9-107.13 the dot is part of the name, not an extension.
|
||||
string bare = Path.GetFileNameWithoutExtension(name);
|
||||
var physics = Read(folder, name)
|
||||
?? (bare.Length > 0 && bare != name ? Read(folder, bare) : null);
|
||||
_cache[key] = physics;
|
||||
return physics;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ public partial class MainWindow : Window
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
InitializeComponent();
|
||||
// The top bar lets the version go first when it runs short of room, which at the usual widths it does;
|
||||
// the title has room for it at any width.
|
||||
Title = $"{Title} {AppInfo.Version}";
|
||||
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaTitleBarHeightHint = TitleBarHeight;
|
||||
|
||||
@@ -10,12 +10,22 @@ namespace StarterNG;
|
||||
static class Program
|
||||
{
|
||||
|
||||
private static bool _softwareRender;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Ahead of everything that could draw: what lies beside the starter is the simulator's.
|
||||
var graphics = SystemGraphicsLibraries.TakeOnWindows();
|
||||
graphics?.ServeTo(typeof(Win32PlatformOptions).Assembly);
|
||||
|
||||
_softwareRender = SoftwareRender.AskedIn(args);
|
||||
|
||||
// Composition root first: everything below, including the crash handlers,
|
||||
// resolves its collaborators from this graph.
|
||||
AppServices.Initialize();
|
||||
if (graphics?.Note is { } note)
|
||||
Diagnostics.Log(note);
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
||||
{
|
||||
@@ -63,9 +73,13 @@ static class Program
|
||||
};
|
||||
}
|
||||
|
||||
private static AppBuilder WithSoftwareRenderIfAsked(this AppBuilder builder) =>
|
||||
_softwareRender ? SoftwareRender.ApplyTo(builder) : builder;
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithSoftwareRenderIfAsked()
|
||||
.WithInterFont()
|
||||
.WithDiagnosticsLogging()
|
||||
.AfterSetup(_ => LogUiExceptions());
|
||||
|
||||
@@ -1009,12 +1009,14 @@ public partial class Scenarios : UserControl
|
||||
int thumbH = AppServices.Current.Settings.LargeThumbnails ? 64 : 32;
|
||||
|
||||
var bmp = LoadMini(miniName, thumbH);
|
||||
// Fill, not Uniform: at a scale such as 125 % layout rounds the slot up to whole pixels,
|
||||
// and a mini keeping its own width inside it left a line of the background between two vehicles.
|
||||
Control visual = bmp is not null
|
||||
? MiniTextures.Sharp(new Image
|
||||
{
|
||||
Source = bmp,
|
||||
Height = thumbH,
|
||||
Stretch = Avalonia.Media.Stretch.Uniform,
|
||||
Stretch = Avalonia.Media.Stretch.Fill,
|
||||
Margin = new Thickness(0)
|
||||
})
|
||||
: MiniTextures.MissingVisual(train.SkinFile, thumbH);
|
||||
@@ -1022,6 +1024,22 @@ public partial class Scenarios : UserControl
|
||||
bool selected = !string.IsNullOrEmpty(train.Name) &&
|
||||
string.Equals(train.Name, AppServices.Current.State.StartingVehicleName,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
// The vehicle started in gets a frame over its picture as well as the lines above and below:
|
||||
// two thin lines alone were hard to see. Drawn over the picture, it moves no vehicle aside.
|
||||
if (selected)
|
||||
visual = new Panel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
visual,
|
||||
new Border
|
||||
{
|
||||
BorderThickness = new Thickness(3),
|
||||
BorderBrush = SelectedVehicleBrush,
|
||||
IsHitTestVisible = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var slot = new Border
|
||||
{
|
||||
@@ -1210,7 +1228,9 @@ public partial class Scenarios : UserControl
|
||||
var source = consistStack.Children[i];
|
||||
// The ghost mirrors what the slot shows, stand-in wagons included, so
|
||||
// dragging a vehicle with no thumbnail still carries something visible.
|
||||
Control copy = source is Border { Child: Image image }
|
||||
// The picked vehicle's picture sits under its frame.
|
||||
var shown = source is Border { Child: Panel { Children: [Image framed, ..] } } ? framed : (source as Border)?.Child;
|
||||
Control copy = shown is Image image
|
||||
? new Image { Source = image.Source, Height = image.Height, Stretch = image.Stretch }
|
||||
: MiniTextures.MissingVisual(null, (int)source.Bounds.Height);
|
||||
panel.Children.Add(copy);
|
||||
|
||||
Reference in New Issue
Block a user