Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eaf3cb3
Revise README for LuaTools App and usage instructions
madoiscool Aug 13, 2026
1079e4d
Create CONTRIBUTING.md
PeeblyWeeb Aug 13, 2026
878812e
Update README.md
PeeblyWeeb Aug 13, 2026
e254e1c
Update README.md
PeeblyWeeb Aug 13, 2026
53abd72
Polish wording and localize error messages
madoiscool Aug 13, 2026
47a703e
Merge branch 'main' of https://github.com/madoiscool/LuaTools
madoiscool Aug 13, 2026
fc27421
Update README.md
PeeblyWeeb Aug 13, 2026
31d5103
Update README.md
PeeblyWeeb Aug 13, 2026
c8fb36e
stats
madoiscool Aug 14, 2026
297e2b0
1.2.8 ig
madoiscool Aug 14, 2026
b476fd0
center vert
madoiscool Aug 14, 2026
9727ea8
gittt border bye
madoiscool Aug 14, 2026
9f851ce
revert to peeblies shit lowk
madoiscool Aug 14, 2026
c3e8bde
Update Strings.it.resx
gattoooo Aug 14, 2026
eb0546c
Merge pull request #21 from gattoooo/patch-1
madoiscool Aug 20, 2026
fb82854
start download shit and and some penis shi
madoiscool Aug 20, 2026
c76486a
Add depot downloader and depot download UI
madoiscool Aug 23, 2026
6ee8996
Add AssetHash, tool caching, and depot download flow
madoiscool Aug 28, 2026
286aa62
Add SteamAutoCrack support and depot fixes
madoiscool Aug 29, 2026
66cb56b
Update README.md
madoiscool Aug 30, 2026
74c4c1b
Merge pull request #41 from madoiscool/downloads
madoiscool Aug 30, 2026
28adc00
oops fogor
madoiscool Aug 30, 2026
ad04cab
typo
madoiscool Aug 30, 2026
ce47541
Restyle mode selection badges with icons and clearer colours
gattoooo Aug 30, 2026
18737f8
fire
PeeblyWeeb Sep 4, 2026
e5e4a58
Update README.md
PeeblyWeeb Sep 4, 2026
e26e027
Add revert for applied fixes and a My games filter
Brandher58 Sep 8, 2026
a6aacb7
Add Fixes translations; secure manifest handling ty claude
madoiscool Sep 8, 2026
4d44df1
fixes hashes and manifests
madoiscool Sep 9, 2026
5e8c8ab
Fix Steam appdetails matching by appid
madoiscool Sep 25, 2026
5f72974
Add Cloudflare DNS fallback
madoiscool Sep 25, 2026
0a61d52
Let manifest sources be declared in JSON
cheapmanga Sep 6, 2026
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ AGENTS.md
*.suo
TestResults/

# Third-party binary reference material — kept locally, never committed.
# Third-party binary reference material. Kept locally, never committed.
# These are other projects' compiled artifacts (no redistribution licence) and
# nothing in the build references them.
OpenSteamTool-releases/
Expand Down
84 changes: 84 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
## Building from source / Developing

```powershell
dotnet build LuaToolsGui.sln
dotnet run --project src/LuaToolsGui/LuaToolsGui.csproj
```

For iterative development:

```powershell
dotnet watch --project src/LuaToolsGui/LuaToolsGui.csproj
```

### Test

```powershell
dotnet test
```

### Releases

Official builds are packaged and signed by the maintainers, so the release tooling isn't part of
this repo. Released builds are framework-dependent (~10 MB) and the setup auto-installs the .NET 8
Desktop Runtime on a clean machine; the app then self-updates through Velopack.

To produce a local build for testing, `dotnet publish -c Release` is enough.

### Layout

| Path | Contents |
|---|---|
| `src/LuaToolsGui/` | The application: `Views/` (XAML), `ViewModels/`, `Services/`, `Models/`, `Resources/` (localization) |
| `src/LuaToolsGui/AppConfig.cs` | All compiled-in endpoints, mirrors and public client values |
| `tests/LuaToolsGui.Tests/` | xUnit tests |
| `scripts/check-i18n.py` | Translation validator, run by CI on every RESX change |

## Contribution Guidelines

Three rules are non-negotiable.

### 1. Every user-facing string must be localized

The app ships 29 languages. **Never hardcode a visible string**, whether in XAML, a toast, a
`MessageBox`, or an empty-state label. Adding text means:

1. Add the key to the English source, `src/LuaToolsGui/Resources/Strings.resx`.
2. Add the accessor to the hand-maintained `Strings.Designer.cs`
(`public static string Key => Get(nameof(Key));`).
3. Add the same key, with a real translation, to all `Strings.<tag>.resx` files. A key missing
from any one of them is a bug.
4. Reference it. XAML: `Text="{x:Static res:Strings.Key}"`; C#: `Resources.Strings.Key` or
`string.Format(Resources.Strings.Key, arg)`.

`.github/workflows/i18n-check.yml` runs `scripts/check-i18n.py` on every PR touching the RESX files.
Translation contributions are pull requests against those files. See
[`src/LuaToolsGui/Resources/README.md`](src/LuaToolsGui/Resources/README.md) for the translator
guide, including which terms to leave untranslated and why.

### 2. Every GitHub request goes through `GithubProxy`

The app must work in regions where `github.com` and `api.github.com` are blocked. **Never call
GitHub directly.** Route through `Services/GithubProxy.cs` (a DI singleton), which tries the direct
URL first and then falls through capability-matched mirrors:

```csharp
await gh.SendAsync(url, ct); // API / metadata
await gh.DownloadAsync(url, dest, progress, ct); // release-asset binaries
```

The Velopack auto-updater is covered too. `UpdateService` passes a `ProxiedFileDownloader` that
reuses `GithubProxy.Candidates`, so the update feed and `.nupkg` fall back to mirrors as well.

### 3. Store-page plugin calls go through `callServerMethod`

The injected store-page script runs inside an HTTPS page and cannot raw-`fetch` a localhost
backend; mixed-content policy kills it silently. Any new store-page → backend call must go through
`Millennium.callServerMethod("luatools", "<Name>", args)` and needs a matching entry in
`CefInjectorService.CallBackendMethod`'s `methodMap`. An unmapped name silently no-ops instead of
erroring.

`Millennium.callServerMethod` resolves one of two ways depending on the user's setup: under
Millennium it's the framework's own object, and otherwise it's the queue-based polyfill
`CefInjectorService` injects ahead of the plugin script. Either way the actual HTTP request is made
outside the browser, which is what sidesteps the mixed-content block.
150 changes: 49 additions & 101 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,116 +1,64 @@
# LuaTools GUI

A Windows desktop client for managing Steam manifest/lua configurations, built with WPF on .NET 8.

LuaTools browses and installs manifest sources, edits `stplug-in` lua files (depot pinning,
per-depot enable/disable), manages unlocker modes, and injects a companion plugin into Steam's
store pages. It ships fully localized in 29 languages and auto-updates via Velopack.
<p align="center">
<img width="1920" alt="lt" src="https://github.com/user-attachments/assets/658f539a-f4a9-4ad5-a3a2-6bb7aa6809bd" />

</p>

# LuaTools
<p>
<img align="right" height="250" src="https://github.com/user-attachments/assets/df083fb0-9be7-4690-9f0f-c8b0a73da881" />

[Discord](https://discord.gg/luatools) • [Website](https://lua.tools) • [Git Mirror](https://git.lua.tools/luatools)

A Windows desktop client for managing Steam manifest/lua configurations, built with WPF on .NET 8.

LuaTools browses and installs manifest sources, edits `stplug-in` lua files (depot pinning,
per-depot enable/disable), manages unlocker modes, and injects a companion plugin into Steam's
store pages.

It ships fully translated in 29 languages and auto-updates via Velopack.
<br><sub>Found a translation error? Tell us about it over on [Discord](https://discord.gg/luatools)</sub>
</p>

## Statistics
<div>
<img src="https://img.shields.io/github/downloads/madoiscool/luatools/LuaTools-win-Setup.exe?displayAssetName=true&style=for-the-badge" />
<img src="https://img.shields.io/github/downloads/madoiscool/luatools/LuaTools-win-Portable.zip?displayAssetName=true&style=for-the-badge" />
</div>

<a href="https://www.star-history.com/?repos=madoiscool%2Fluatools&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=madoiscool/luatools&type=date&theme=dark&legend=top-left&sealed_token=1SX6CDP2N0Emx5IbGfQmEz4TxM11iXtfLKL9K1utRzINJPEDv55f5XEYjliBUB1No6wbcWbMs-cSzO65OC7kAlMLAHJXjqmDoeRCM6hVtW9xd7fyg8cr2DG4gATwkgym1JvgPs4_PeGi6XMAm7_2CVXU9UxRLBW_GP4-Qmd3-AosSRCM1Nkm7dEr2_Ut" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=madoiscool/luatools&type=date&legend=top-left&sealed_token=1SX6CDP2N0Emx5IbGfQmEz4TxM11iXtfLKL9K1utRzINJPEDv55f5XEYjliBUB1No6wbcWbMs-cSzO65OC7kAlMLAHJXjqmDoeRCM6hVtW9xd7fyg8cr2DG4gATwkgym1JvgPs4_PeGi6XMAm7_2CVXU9UxRLBW_GP4-Qmd3-AosSRCM1Nkm7dEr2_Ut" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=madoiscool/luatools&type=date&legend=top-left&sealed_token=1SX6CDP2N0Emx5IbGfQmEz4TxM11iXtfLKL9K1utRzINJPEDv55f5XEYjliBUB1No6wbcWbMs-cSzO65OC7kAlMLAHJXjqmDoeRCM6hVtW9xd7fyg8cr2DG4gATwkgym1JvgPs4_PeGi6XMAm7_2CVXU9UxRLBW_GP4-Qmd3-AosSRCM1Nkm7dEr2_Ut" />
</picture>
</a>

## Requirements

- Windows 10/11
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (the released installer bundles a
check for the .NET 8 **Desktop Runtime** and installs it if missing; building from source needs
the full SDK)

## Build

```powershell
dotnet build LuaToolsGui.sln
dotnet run --project src/LuaToolsGui/LuaToolsGui.csproj
```

For iterative development:

```powershell
dotnet watch --project src/LuaToolsGui/LuaToolsGui.csproj
```

> **Do not run `dotnet build` while `dotnet watch` is running.** They collide on `obj/` and produce
> spurious `CS0579` duplicate-attribute errors from the generated `_wpftmp` project. Let watch do
> the building, or stop it first.
>
> Editing a `.resx` file or any `x:Static` XAML binding requires a **watch restart**. Hot reload
> can't compile new RESX into satellite assemblies or re-parse compile-time `x:Static`.

## Test

```powershell
dotnet test
```

## Releases

Official builds are packaged and signed by the maintainers, so the release tooling isn't part of
this repo. Released builds are framework-dependent (~10 MB) and the setup auto-installs the .NET 8
Desktop Runtime on a clean machine; the app then self-updates through Velopack.

To produce a local build for testing, `dotnet publish -c Release` is enough.

## Layout

| Path | Contents |
|---|---|
| `src/LuaToolsGui/` | The application: `Views/` (XAML), `ViewModels/`, `Services/`, `Models/`, `Resources/` (localization) |
| `src/LuaToolsGui/AppConfig.cs` | All compiled-in endpoints, mirrors and public client values |
| `tests/LuaToolsGui.Tests/` | xUnit tests |
| `scripts/check-i18n.py` | Translation validator, run by CI on every RESX change |
check for the .NET 8 **Desktop Runtime** and installs it if missing; [building from source](https://github.com/madoiscool/LuaTools/blob/main/CONTRIBUTING.md#building-from-source--developing) needs
the full SDK

## Contributing
## Installation
You can find release builds on the [luatools website](https://lua.tools/app) or in the [releases](https://github.com/madoiscool/LuaTools/releases/latest) tab.

Three rules are non-negotiable.

### 1. Every user-facing string must be localized

The app ships 29 languages. **Never hardcode a visible string**, whether in XAML, a toast, a
`MessageBox`, or an empty-state label. Adding text means:

1. Add the key to the English source, `src/LuaToolsGui/Resources/Strings.resx`.
2. Add the accessor to the hand-maintained `Strings.Designer.cs`
(`public static string Key => Get(nameof(Key));`).
3. Add the same key, with a real translation, to all `Strings.<tag>.resx` files. A key missing
from any one of them is a bug.
4. Reference it. XAML: `Text="{x:Static res:Strings.Key}"`; C#: `Resources.Strings.Key` or
`string.Format(Resources.Strings.Key, arg)`.

`.github/workflows/i18n-check.yml` runs `scripts/check-i18n.py` on every PR touching the RESX files.
Translation contributions are pull requests against those files. See
[`src/LuaToolsGui/Resources/README.md`](src/LuaToolsGui/Resources/README.md) for the translator
guide, including which terms to leave untranslated and why.

### 2. Every GitHub request goes through `GithubProxy`

The app must work in regions where `github.com` and `api.github.com` are blocked. **Never call
GitHub directly.** Route through `Services/GithubProxy.cs` (a DI singleton), which tries the direct
URL first and then falls through capability-matched mirrors:

```csharp
await gh.SendAsync(url, ct); // API / metadata
await gh.DownloadAsync(url, dest, progress, ct); // release-asset binaries
```

The Velopack auto-updater is covered too. `UpdateService` passes a `ProxiedFileDownloader` that
reuses `GithubProxy.Candidates`, so the update feed and `.nupkg` fall back to mirrors as well.

### 3. Store-page plugin calls go through `callServerMethod`

The injected store-page script runs inside an HTTPS page and cannot raw-`fetch` a localhost
backend; mixed-content policy kills it silently. Any new store-page → backend call must go through
`Millennium.callServerMethod("luatools", "<Name>", args)` and needs a matching entry in
`CefInjectorService.CallBackendMethod`'s `methodMap`. An unmapped name silently no-ops instead of
erroring.

`Millennium.callServerMethod` resolves one of two ways depending on the user's setup: under
Millennium it's the framework's own object, and otherwise it's the queue-based polyfill
`CefInjectorService` injects ahead of the plugin script. Either way the actual HTTP request is made
outside the browser, which is what sidesteps the mixed-content block.

## Related
## Credits / Adjacent software

- [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 from game executables
- [CloudRedirect](https://github.com/Selectively11/CloudRedirect): Steam Cloud revival project, can be turned on via the mode page

## Licence

MIT. See [LICENSE](LICENSE).

<img width="100%" alt="928c14bad5bbc258894b050af1e17ba8" src="https://github.com/user-attachments/assets/90ed4a2b-6fec-4afa-a56d-4983ea190ddb" />

56 changes: 56 additions & 0 deletions SOURCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Manifest sources as JSON

Extra manifest sources can be declared in `.json` files under
`%AppData%\LuaToolsGui\sources\`. A source declared this way appears as a row on the **Add** page and
installs through the same pipeline as any other.

Settings → *Manifest sources* lists the files found, what each contributed, and why anything was
refused. Files are re-read whenever that page is opened, so there is nothing to restart.

## Why

The sources the app can fetch from are otherwise fixed at build time. Following a repo that moved, or
adding a community one, means cutting a release. This makes it a line of JSON.

## Data only

A file can say **where** manifests are fetched from. It cannot supply code, a binary, or a fetch routine
of its own: it names one of the shapes the app already knows how to consume, and the app does the
fetching. Installing one of these files cannot execute anything.

## Format

`%AppData%\LuaToolsGui\sources\example.json`:

```json
{
"schema": 1,
"name": "Example sources",
"author": "you",
"sources": [
{
"name": "example-zip",
"displayName": "Example",
"kind": "manifestZip",
"url": "https://raw.githubusercontent.com/someone/some-repo/main/{appid}.zip",
"mirrors": ["https://cdn.jsdelivr.net/gh/someone/some-repo@main/{appid}.zip"],
"badge": "Free"
}
]
}
```

| Field | |
|---|---|
| `kind` | `manifestZip` — one `<appid>.zip` holding the lua and its `.manifest` files.<br>`luaFile` — one `<appid>.lua`, entitlements and depot keys only. |
| `url` | Must be `https` and contain `{appid}`. |
| `mirrors` | Tried in order when the primary is unreachable. Optional. |
| `displayName` | Row label. Defaults to `name`. |
| `badge` | Short label on the row. Cosmetic. Optional. |

A source is refused, with the reason shown in Settings, if it uses a name the app already uses, is not
`https`, has no `{appid}`, or names a `kind` that does not exist. One bad entry never takes the file's
good ones down with it.

GitHub urls go through the app's existing mirror fallback, the availability check included. A host that
refuses `HEAD` is probed with a one-byte ranged `GET` instead.
8 changes: 4 additions & 4 deletions scripts/check-i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
PENDING_TRANSLATION: set[str] = set()
# Empty on purpose: every key is translated in all 29 languages, so the parity check above is
# unconditional. Add a key here ONLY while its feature's UI is still moving, and clear it again
# as soon as the translations land — anything listed is English-only for every user.
# as soon as the translations land. Anything listed is English-only for every user.



Expand Down Expand Up @@ -60,7 +60,7 @@ def main():
try:
tr = parse(path)
except ET.ParseError as e:
problems.append(f"{name}: INVALID XML — {e}")
problems.append(f"{name}: INVALID XML. {e}")
continue

missing = base_keys - set(tr) - PENDING_TRANSLATION
Expand Down Expand Up @@ -95,8 +95,8 @@ def main():
print(" -", k)
stale = PENDING_TRANSLATION - base_keys
if stale:
print(f"\nNOTE: {len(stale)} PENDING_TRANSLATION entr(ies) no longer exist in Strings.resx — "
f"drop them from the list: {', '.join(sorted(stale))}")
print(f"\nNOTE: {len(stale)} PENDING_TRANSLATION entr(ies) no longer exist in Strings.resx. "
f"Drop them from the list: {', '.join(sorted(stale))}")
return 0


Expand Down
Loading