Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
groups:
actions-deps:
patterns:
Expand All @@ -19,7 +19,7 @@ updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
target-branch: "develop"
open-pull-requests-limit: 1
groups:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,6 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
.idea

.local_deploy
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PropertyGroup>
<Product>ModVerify</Product>
<Authors>Alamo Engine Tools and Contributors</Authors>
<Copyright>Copyright © 2025 Alamo Engine Tools and contributors. All rights reserved.</Copyright>
<Copyright>Copyright © 2026 Alamo Engine Tools and contributors. All rights reserved.</Copyright>
<PackageProjectUrl>https://github.com/AlamoEngine-Tools/ModVerify</PackageProjectUrl>
<LicenseFile>$(RepoRootPath)LICENSE</LicenseFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand All @@ -33,7 +33,7 @@

<ItemGroup>
<PackageReference Update="SauceControl.InheritDoc" Version="2.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Alamo Engine Tools
Copyright (c) 2026 Alamo Engine Tools

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
75 changes: 75 additions & 0 deletions deploy-local.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Local deployment script for ModVerify to test the update feature.
# This script builds the application, creates an update manifest, and "deploys" it to a local directory.

$ErrorActionPreference = "Stop"

$root = $PSScriptRoot
if ([string]::IsNullOrEmpty($root)) { $root = Get-Location }

$deployRoot = Join-Path $root ".local_deploy"
$stagingDir = Join-Path $deployRoot "staging"
$serverDir = Join-Path $deployRoot "server"
$installDir = Join-Path $deployRoot "install"

$toolProj = Join-Path $root "src\ModVerify.CliApp\ModVerify.CliApp.csproj"
$creatorProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\ApplicationManifestCreator\ApplicationManifestCreator.csproj"
$uploaderProj = Join-Path $root "modules\ModdingToolBase\src\AnakinApps\FtpUploader\FtpUploader.csproj"

$toolExe = "ModVerify.exe"
$updaterExe = "AnakinRaW.ExternalUpdater.exe"
$manifestCreatorDll = "AnakinRaW.ApplicationManifestCreator.dll"
$uploaderDll = "AnakinRaW.FtpUploader.dll"

# 1. Clean and Create directories
if (Test-Path $deployRoot) { Remove-Item -Recurse -Force $deployRoot }
New-Item -ItemType Directory -Path $stagingDir | Out-Null
New-Item -ItemType Directory -Path $serverDir | Out-Null
New-Item -ItemType Directory -Path $installDir | Out-Null

Write-Host "--- Building ModVerify (net481) ---" -ForegroundColor Cyan
dotnet build $toolProj --configuration Release -f net481 --output "$deployRoot\bin\tool" /p:DebugType=None /p:DebugSymbols=false

Write-Host "--- Building Manifest Creator ---" -ForegroundColor Cyan
dotnet build $creatorProj --configuration Release --output "$deployRoot\bin\creator"

Write-Host "--- Building Local Uploader ---" -ForegroundColor Cyan
dotnet build $uploaderProj --configuration Release --output "$deployRoot\bin\uploader"

# 2. Prepare staging
Write-Host "--- Preparing Staging ---" -ForegroundColor Cyan
Copy-Item "$deployRoot\bin\tool\$toolExe" $stagingDir
Copy-Item "$deployRoot\bin\tool\$updaterExe" $stagingDir

# 3. Create Manifest
# Origin must be an absolute URI for the manifest creator.
# Using 127.0.0.1 and file:// is tricky with Flurl/DownloadManager sometimes.
# We'll use the local path and ensure it's formatted correctly.
$serverPath = (Resolve-Path $serverDir).Path
$serverUri = "file:///$($serverPath.Replace('\', '/'))"
# If we have 3 slashes, Flurl/DownloadManager might still fail on Windows if it expects a certain format.
# However, the ManifestCreator just needs a valid URI for the 'Origin' field in the manifest.
Write-Host "--- Creating Manifest (Origin: $serverUri) ---" -ForegroundColor Cyan
dotnet "$deployRoot\bin\creator\$manifestCreatorDll" `
-a "$stagingDir\$toolExe" `
--appDataFiles "$stagingDir\$updaterExe" `
--origin "$serverUri" `
-o "$stagingDir" `
-b "beta"

# 4. "Deploy" to server using the local uploader
Write-Host "--- Deploying to Local Server ---" -ForegroundColor Cyan
dotnet "$deployRoot\bin\uploader\$uploaderDll" local --base "$serverDir" --source "$stagingDir"

# 5. Setup a "test" installation
Write-Host "--- Setting up Test Installation ---" -ForegroundColor Cyan
Copy-Item "$deployRoot\bin\tool\*" $installDir -Recurse

Write-Host "`nLocal deployment complete!" -ForegroundColor Green
Write-Host "Server directory: $serverDir"
Write-Host "Install directory: $installDir"
Write-Host "`nTo test the update:"
Write-Host "1. (Optional) Modify the version in version.json and run this script again to 'push' a new version to the local server."
Write-Host "2. Run ModVerify from the install directory with the following command:"
Write-Host " cd '$installDir'"
Write-Host " .\ModVerify.exe updateApplication --updateManifestUrl '$serverUri'"
Write-Host "`n Note: You can also specify a different branch using --updateBranch if needed."
2 changes: 1 addition & 1 deletion modules/ModdingToolBase
Submodule ModdingToolBase updated 23 files
+1 −1 Directory.Build.props
+65 −1 src/AnakinApps/ApplicationBase.CommandLine/ConsoleUtilities.cs
+1 −1 src/AnakinApps/ApplicationBase.WPF/ApplicationBase.WPF.csproj
+7 −7 src/AnakinApps/ApplicationBase/ApplicationBase.csproj
+5 −5 src/AnakinApps/ApplicationManifestCreator/ApplicationManifestCreator.csproj
+5 −5 src/AnakinApps/FtpUploader/FtpUploader.csproj
+3 −3 src/CommonUtilities/CommonUtilities.WPF.ApplicationFramework/CommonUtilities.WPF.ApplicationFramework.csproj
+3 −3 src/CommonUtilities/CommonUtilities.WPF.Controls/CommonUtilities.WPF.Controls.csproj
+6 −6 src/CommonUtilities/CommonUtilities.WPF.Core/CommonUtilities.WPF.Core.csproj
+1 −1 src/CommonUtilities/CommonUtilities.WPF.Core/DPI/DisplayHelper.cs
+3 −3 src/CommonUtilities/CommonUtilities.WPF.Core/DPI/DisplayInfo.cs
+3 −3 src/CommonUtilities/CommonUtilities.WPF.Core/DPI/DpiHelper.cs
+1 −1 src/CommonUtilities/CommonUtilities.Windows/CommonUtilities.Windows.csproj
+2 −2 src/Updater/AppUpdaterFramework.Manifest/AppUpdaterFramework.Manifest.csproj
+1 −1 src/Updater/AppUpdaterFramework.WPF/AppUpdaterFramework.WPF.csproj
+6 −6 src/Updater/AppUpdaterFramework/AppUpdaterFramework.csproj
+0 −2 src/Updater/AppUpdaterFramework/Updater/Internal/ApplicationUpdater.cs
+4 −4 src/Updater/AppUpdaterFramework/Updater/Internal/UpdateCleanPipeline.cs
+35 −27 src/Updater/AppUpdaterFramework/Updater/Internal/UpdatePipeline.cs
+11 −9 src/Updater/AppUpdaterFramework/Updater/Tasks/DownloadStep.cs
+10 −6 src/Updater/AppUpdaterFramework/Updater/Tasks/InstallStep.cs
+7 −7 src/Updater/ExternalUpdater.App/ExternalUpdater.App.csproj
+3 −3 src/Updater/ExternalUpdater.Core/ExternalUpdater.Core.csproj
140 changes: 105 additions & 35 deletions src/ModVerify.CliApp/GameFinder/GameFinderService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PG.StarWarsGame.Engine;
using PG.StarWarsGame.Infrastructure.Clients.Steam;
using PG.StarWarsGame.Infrastructure.Games;
using PG.StarWarsGame.Infrastructure.Mods;
Expand All @@ -12,6 +14,17 @@

namespace AET.ModVerify.App.GameFinder;

internal class GameFinderSettings
{
internal static readonly GameFinderSettings Default = new();

public bool InitMods { get; init; } = true;

public bool SearchFallbackGame { get; init; } = true;

public GameEngineType? Engine { get; init; } = null;
}

internal class GameFinderService
{
private readonly IServiceProvider _serviceProvider;
Expand All @@ -29,18 +42,46 @@ public GameFinderService(IServiceProvider serviceProvider)
_logger = _serviceProvider.GetService<ILoggerFactory>()?.CreateLogger(GetType());
}

public GameFinderResult FindGames()
public GameFinderResult FindGames(GameFinderSettings settings)
{
var detectors = new List<IGameDetector>
{
new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(Environment.CurrentDirectory), _serviceProvider),
new SteamPetroglyphStarWarsGameDetector(_serviceProvider),
};

return FindGames(detectors);
return FindGames(detectors, settings);
}

public IGame FindGame(string gamePath, GameFinderSettings settings)
{
var detectors = new List<IGameDetector>
{
new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider),
};
return FindGames(detectors, settings).Game;
}

public GameFinderResult FindGamesFromPathOrGlobal(string path)
public bool TryFindGame(string gamePath, GameFinderSettings settings, [NotNullWhen(true)]out IGame? game)
{
var detectors = new List<IGameDetector>
{
new DirectoryGameDetector(_fileSystem.DirectoryInfo.New(gamePath), _serviceProvider),
};

try
{
game = FindGames(detectors, settings).Game;
return true;
}
catch (GameNotFoundException)
{
game = null;
return false;
}
}

public GameFinderResult FindGamesFromPathOrGlobal(string path, GameFinderSettings settings)
{
// There are four common situations:
// 1. path points to the actual game directory
Expand All @@ -62,55 +103,55 @@ public GameFinderResult FindGamesFromPathOrGlobal(string path)

// Cases 3 & 4
detectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider));
return FindGames(detectors);
return FindGames(detectors, settings);
}

private bool TryDetectGame(GameType gameType, IList<IGameDetector> detectors, out GameDetectionResult result)
private GameFinderResult FindGames(IList<IGameDetector> detectors, GameFinderSettings settings)
{
var gd = new CompositeGameDetector(detectors, _serviceProvider);

try
GameDetectionResult? detectionResult = null;
if (settings.Engine is GameEngineType.Eaw)
{
result = gd.Detect(gameType);
if (result.GameLocation is null)
return false;
return true;
_logger?.LogTrace("Trying to find requested EaW installation.");
if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult))
{
var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?");
_logger?.LogTrace(e, e.Message);
throw e;
}
}
catch (Exception e)
{
result = GameDetectionResult.NotInstalled(gameType);
_logger?.LogTrace("Unable to find game installation: {Message}", e.Message);
return false;
}
}

private GameFinderResult FindGames(IList<IGameDetector> detectors)
{
// FoC needs to be tried first
if (!TryDetectGame(GameType.Foc, detectors, out var result))

if (detectionResult is null && !TryDetectGame(GameType.Foc, detectors, out detectionResult))
{
if (settings.Engine is GameEngineType.Foc)
{
var e = new GameNotFoundException($"Unable to find requested game installation '{settings.Engine}'. Wrong install path?");
_logger?.LogTrace(e, e.Message);
throw e;
}

// If the engine is unspecified, we also need to check for EaW.
_logger?.LogTrace("Unable to find FoC installation. Trying again with EaW...");
if (!TryDetectGame(GameType.Eaw, detectors, out result))
if (!TryDetectGame(GameType.Eaw, detectors, out detectionResult))
throw new GameNotFoundException("Unable to find game installation: Wrong install path?");
}

if (result.GameLocation is null)
if (detectionResult.GameLocation is null)
throw new GameNotFoundException("Unable to find game installation: Wrong install path?");

_logger?.LogInformation(ModVerifyConstants.ConsoleEventId,
"Found game installation: {ResultGameIdentity} at {GameLocationFullName}", result.GameIdentity, result.GameLocation.FullName);
"Found game installation: {ResultGameIdentity} at {GameLocationFullName}", detectionResult.GameIdentity, detectionResult.GameLocation.FullName);

var game = _gameFactory.CreateGame(result, CultureInfo.InvariantCulture);
var game = _gameFactory.CreateGame(detectionResult, CultureInfo.InvariantCulture);

SetupMods(game);
if (settings.InitMods)
SetupMods(game);


IGame? fallbackGame = null;
// If the game is Foc we want to set up Eaw as well as the fallbackGame
if (game.Type == GameType.Foc)
if (SearchForFallbackGame(settings, detectionResult))
{
var fallbackDetectors = new List<IGameDetector>();

if (game.Platform == GamePlatform.SteamGold)
fallbackDetectors.Add(new SteamPetroglyphStarWarsGameDetector(_serviceProvider));
else
Expand All @@ -119,17 +160,46 @@ private GameFinderResult FindGames(IList<IGameDetector> detectors)
if (!TryDetectGame(GameType.Eaw, fallbackDetectors, out var fallbackResult) || fallbackResult.GameLocation is null)
throw new GameNotFoundException("Unable to find fallback game installation: Wrong install path?");

_logger?.LogInformation(ModVerifyConstants.ConsoleEventId,
_logger?.LogInformation(ModVerifyConstants.ConsoleEventId,
"Found fallback game installation: {FallbackResultGameIdentity} at {GameLocationFullName}", fallbackResult.GameIdentity, fallbackResult.GameLocation.FullName);

fallbackGame = _gameFactory.CreateGame(fallbackResult, CultureInfo.InvariantCulture);

SetupMods(fallbackGame);
if (settings.InitMods)
SetupMods(fallbackGame);
}

return new GameFinderResult(game, fallbackGame);
}

private static bool SearchForFallbackGame(GameFinderSettings settings, GameDetectionResult? foundGame)
{
if (settings.Engine is GameEngineType.Eaw)
return false;
if (foundGame is { Installed: true, GameIdentity.Type: GameType.Eaw })
return false;
return settings.SearchFallbackGame;
}

private bool TryDetectGame(GameType gameType, IList<IGameDetector> detectors, out GameDetectionResult result)
{
var gd = new CompositeGameDetector(detectors, _serviceProvider);

try
{
result = gd.Detect(gameType);
if (result.GameLocation is null)
return false;
return true;
}
catch (Exception e)
{
result = GameDetectionResult.NotInstalled(gameType);
_logger?.LogTrace("Unable to find game installation: {Message}", e.Message);
return false;
}
}

private void SetupMods(IGame game)
{
var modFinder = _serviceProvider.GetRequiredService<IModFinder>();
Expand Down
Loading
Loading