Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ You can find release builds on the [luatools website](https://lua.tools/app) or
- [Millennium](https://steambrew.app/): the Steam plugin framework whose injection API this app
polyfills when Millennium isn't installed
- [Velopack](https://velopack.io/): installer and auto-update framework
- [DepotDownloaderMod](https://github.com/SteamAutoCracks/DepotDownloaderMod): downloads depot content
from Steam's CDN, powering the Depots page's Download action. A fork of
[DepotDownloader](https://github.com/SteamRE/DepotDownloader), fetched and run as a standalone tool
- [SteamAutoCrack](https://github.com/SteamAutoCracks/Steam-auto-crack): fetched and launched from the
Downloads page
- [Steamless](https://github.com/atom0s/Steamless): removes SteamStub DRM from game executables
- [CloudRedirect](https://github.com/Selectively11/CloudRedirect): Steam Cloud redirection, used by the
mode install flow

## Licence

Expand Down
39 changes: 37 additions & 2 deletions src/LuaToolsGui/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using System.Windows.Threading;
using LuaToolsGui.Models;
using LuaToolsGui.Services;
Expand Down Expand Up @@ -40,14 +40,22 @@ public App()
services.AddSingleton<GithubProxy>();
services.AddSingleton<HardwareAppIdService>();
services.AddSingleton<SteamlessService>();
services.AddSingleton<SteamAutoCrackService>();
services.AddSingleton<CloudRedirectService>();
services.AddSingleton<DepotDownloaderService>();
services.AddSingleton<UnlockerService>();
services.AddSingleton<PluginInstallerService>();
services.AddTransient<DropInstallViewModel>(); // one per page (Home, Add)
services.AddSingleton<AuthService>();
services.AddSingleton<LuaToolsApiClient>();
services.AddSingleton<HubcapService>();
services.AddSingleton<UpdateService>();
// Central download queue. Singleton + hosted service (same pattern as HttpServerService
// below): the hosted lifetime runs the scheduler pump, and view models resolve the same
// instance to enqueue and observe.
services.AddSingleton<Services.Downloads.DownloadQueue>();
services.AddHostedService(sp => sp.GetRequiredService<Services.Downloads.DownloadQueue>());
services.AddSingleton<Services.Downloads.ManifestJobFactory>();
// Hook loader infrastructure
services.AddSingleton<PluginAddService>();
services.AddSingleton<HttpServerService>();
Expand All @@ -64,12 +72,14 @@ public App()
services.AddSingleton<HomeViewModel>();
services.AddSingleton<ModeViewModel>();
services.AddSingleton<FixesViewModel>();
services.AddSingleton<DownloadsViewModel>();
services.AddSingleton<PluginViewModel>();
services.AddSingleton<OnboardingViewModel>();
services.AddSingleton<MainViewModel>();
// Pages resolved by NavigationView via the DI service provider.
services.AddSingleton<HomeView>();
services.AddSingleton<DownloadView>();
services.AddSingleton<DownloadsView>();
services.AddSingleton<ManageView>();
services.AddSingleton<BuildsView>();
services.AddSingleton<ModeView>();
Expand Down Expand Up @@ -215,6 +225,8 @@ protected override async void OnStartup(StartupEventArgs e)

// Legacy cleanup: older builds staged downloads in ~/Downloads/LuaTools (they now stage in
// %TEMP% and self-delete). Remove any leftovers from that user-visible folder, best-effort.
// Also sweep the current %TEMP% staging folder: a crash mid-download, or an overwrite confirm
// the user never answered, leaves a staged zip nobody will ever delete.
_ = System.Threading.Tasks.Task.Run(() =>
{
try
Expand All @@ -224,6 +236,8 @@ protected override async void OnStartup(StartupEventArgs e)
if (System.IO.Directory.Exists(legacy)) System.IO.Directory.Delete(legacy, recursive: true);
}
catch { /* best effort, never block startup on cleanup */ }

Services.Downloads.HttpFileDownloader.SweepStale();
});

await _host.StartAsync();
Expand All @@ -237,6 +251,12 @@ protected override async void OnStartup(StartupEventArgs e)
if (ModeMigration.Apply(_host.Services.GetRequiredService<SettingsService>()))
_host.Services.GetRequiredService<CacheService>().OnboardingComplete = false;

// Point OST/BST at config/stplug-in so lua writes hot-reload. Must run AFTER the migration
// above, which is what makes SelectedMode parse. The app no longer tells anyone to restart
// Steam for a lua change, so this registration is what makes that promise true — and it
// previously only ever ran during a mode install through this app.
_host.Services.GetRequiredService<UnlockerService>().EnsureLuaPathRegistered();

var main = _host.Services.GetRequiredService<MainViewModel>();
var settingsVm = _host.Services.GetRequiredService<SettingsViewModel>();

Expand Down Expand Up @@ -344,8 +364,20 @@ protected override async void OnStartup(StartupEventArgs e)
Dispatcher.Invoke(() => { window.NavigateToManage(); _ = manage.OpenDetailForAppIdAsync(appId); });
var home = _host.Services.GetRequiredService<HomeViewModel>();
home.NavigateToGame = openInManage;

// Deliberately NO queue-wide completion toast here. Every entry point already reports its own
// outcome: Fixes toasts from ManifestJobFactory, the Add page shows its InstallStatus banner, the
// store plugin has its popup, and a silent install pops a tray balloon. A global toast on top of
// those double-notified every one of them.

// The Downloads tab's "Review" button on an item waiting for an overwrite confirmation: the
// overlay lives on the Add page, so send the user there.
_host.Services.GetRequiredService<DownloadsViewModel>().RevealItem = _ => window.NavigateToAdd();

download.NavigateToGame = openInManage;
builds.NavigateToManage = openInManage; // Builds "Manage" button: the reverse of "Manage Build"
// Depot download queues one item covering the whole selection; show the user where it went.
builds.RequestShowDownloads = () => Dispatcher.Invoke(window.NavigateToDownloads);

// Dragging a SteamDB / Steam store link onto either drop box installs that appid. Routed through
// HandleProtocolUrl rather than calling ProtocolInstall directly, so a dropped link and
Expand Down Expand Up @@ -514,7 +546,10 @@ private void HandleProtocolUrl(string url)
{
window.ShowInstallNotification(msg, error);
// Cold launch + no tray app wanted → exit once the balloon has had time to show.
if (_exitAfterSilentInstall)
// ProtocolInstall already awaited this item's completion, but the user (or the
// store plugin) may have queued more; exiting now would cancel them mid-flight.
var queue = _host.Services.GetRequiredService<Services.Downloads.DownloadQueue>();
if (_exitAfterSilentInstall && queue.ActiveCount == 0)
_ = Task.Delay(6000).ContinueWith(_ => Dispatcher.Invoke(Shutdown));
}));
}
Expand Down
29 changes: 29 additions & 0 deletions src/LuaToolsGui/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ public static class AppConfig
// CloudRedirect (Selectively11): the Mode page "Manage" button downloads the latest CloudRedirect.exe
// GUI manager from here and launches it. (Separate from the CLI fixer used by the mode install flow.)
public const string CloudRedirectRepo = "Selectively11/CloudRedirect";

// SteamAutoCrack: the Downloads page button fetches this release and launches its GUI.
//
// Three facts about the shipped asset drive how SteamAutoCrackService handles it:
// * GUI ONLY. The release contains a single exe and no SteamAutoCrack.CLI.exe, and that GUI parses
// no command-line arguments, so we can open it and nothing more. There is no way to drive a
// crack from here; the user does everything in their own window.
// * FRAMEWORK-DEPENDENT net10.0-windows. It is a single-file bundle but the runtime is NOT inside
// it (no hostpolicy/coreclr/System.Private.CoreLib, no includedFrameworks), so the .NET 10
// DESKTOP runtime must be present. We install it on demand via Velopack's Runtimes API.
// * Its zip has a real directory TREE (Goldberg/, TEMP/ beside the exe), unlike the flat zip
// DepotDownloaderService expects. Their exe resolves those paths from its own base directory,
// so extract without flattening.
public const string SteamAutoCrackRepo = "SteamAutoCracks/Steam-auto-crack";

// DepotDownloaderMod: downloads raw depot content from Steam's CDN using depot keys + a local manifest.
// Powers the Depots page "Download" action.
//
// This is a RE-PACK we host ourselves, NOT the upstream SteamAutoCracks/DepotDownloaderMod repo, for two
// reasons: upstream ships its assets as `Release.rar` (System.IO.Compression cannot read RAR), and its
// build is framework-dependent net9.0 while this app is net8.0-windows, so users without the .NET 9
// runtime couldn't run it. Note that adding a RAR reader would fix only the FIRST of those.
//
// The re-pack is produced automatically by the `repack.yml` workflow in that repo: on each upstream
// release it rebuilds their source at that tag as a SELF-CONTAINED win-x64 publish and uploads a FLAT
// zip. Both properties are load-bearing for DepotDownloaderService.EnsureToolAsync, which takes the
// first `.zip` asset, extracts it straight into ToolDir, and then expects DepotDownloaderMod.exe at the
// archive ROOT (unlike PluginInstallerService, it has no single-top-level-folder hoisting).
public const string DepotDownloaderRepo = "mendy-tools/DepotDownloaderMod";
public const string ManifestBackendUrl = "http://167.235.229.108";
public const string ManifestBackendUserAgent = "secretgoonpoon";

Expand Down
7 changes: 6 additions & 1 deletion src/LuaToolsGui/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ui:FluentWindow
<ui:FluentWindow
x:Class="LuaToolsGui.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Expand Down Expand Up @@ -100,6 +100,11 @@
<ui:SymbolIcon Symbol="ArrowClockwise24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{x:Static res:Strings.Nav_Downloads}" TargetPageType="{x:Type views:DownloadsView}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="ArrowDownload24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{x:Static res:Strings.Nav_Settings}" TargetPageType="{x:Type views:SettingsView}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Settings24" />
Expand Down
5 changes: 4 additions & 1 deletion src/LuaToolsGui/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Windows;
using System.Windows;
using LuaToolsGui.Services;
using LuaToolsGui.ViewModels;
using LuaToolsGui.Views;
Expand Down Expand Up @@ -118,6 +118,9 @@ public void ShowInstallNotification(string message, bool error)
/// <summary>Switch to the Add page (used by the Manage page's "Update" action).</summary>
public void NavigateToAdd() => RootNavigation.Navigate(typeof(DownloadView));

/// <summary>Switch to Downloads (used when an item needs the user to resolve something).</summary>
public void NavigateToDownloads() => RootNavigation.Navigate(typeof(DownloadsView));

/// <summary>Switch to Manage (used by Home's "recently added" cards). Caller opens the detail.</summary>
public void NavigateToManage() => RootNavigation.Navigate(typeof(ManageView));

Expand Down
4 changes: 4 additions & 0 deletions src/LuaToolsGui/Models/ModeModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public sealed class GithubAsset
[JsonPropertyName("name")] public string Name { get; set; } = "";
[JsonPropertyName("browser_download_url")] public string DownloadUrl { get; set; } = "";
[JsonPropertyName("digest")] public string? Digest { get; set; } // "sha256:<hex>"

/// <summary>Asset size in bytes. Lets a tool download report real byte counts instead of a bare
/// fraction, since GithubProxy.DownloadAsync only reports 0..1.</summary>
[JsonPropertyName("size")] public long Size { get; set; }
}

/// <summary>Queried state for one mode. What a Mode-page card binds to.</summary>
Expand Down
Loading
Loading