Compare commits

..

12 Commits

128 changed files with 8964 additions and 1323 deletions
-1
View File
@@ -1 +0,0 @@
ko_fi: kitbyte
+70
View File
@@ -0,0 +1,70 @@
name: Bug report
description: Create a report to help us improve
title: '[Bug]: '
labels: ["bug"]
assignees:
- k1tbyte
body:
- type: markdown
attributes:
value: |
**🚨 STOP BEFORE YOU POST: THIS PROJECT HAS NO OFFICIAL YOUTUBE TUTORIALS. 🚨**
If you downloaded an executable from a YouTube video link, you downloaded a virus/password stealer from a scammer. **Run an antivirus immediately and change your passwords.** Do not open issues about stolen accounts here — this project is not related to those videos.
- type: checkboxes
id: scam_check
attributes:
label: ⚠️ Download Source Confirmation (REQUIRED)
description: You must check this box to proceed.
options:
- label: I confirm that I downloaded this tool DIRECTLY from this official GitHub repository, and NOT from a YouTube video, Discord, or any other third-party website.
required: true
- type: input
id: client_version
attributes:
label: Client (Wand) version
description: What version of the client are you using? (e.g., 9.0.0)
validations:
required: true
- type: input
id: enhancer_version
attributes:
label: Enhancer version
description: What version of this tool are you using?
validations:
required: true
- type: textarea
id: bug_description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: false
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
- type: textarea
id: additional_context
attributes:
label: Screenshots & Additional context
description: Drag and drop screenshots here, or add any other context about the problem.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+26
View File
@@ -0,0 +1,26 @@
name: Mirror to GitLab
on:
push:
branches: [ "master", "main" ]
tags:
- '*'
workflow_dispatch:
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push to GitLab
env:
GITLAB_USERNAME: kitbyte
GITLAB_REPO: Wand-Enhancer
run: |
git remote add gitlab https://oauth2:${{ secrets.GITLAB_TOKEN }}@gitlab.com/$GITLAB_USERNAME/$GITLAB_REPO.git
git push gitlab --all --force
git push gitlab --tags --force
+11 -3
View File
@@ -130,8 +130,16 @@ dist
.pnp.*
./WeModPatcher/obj/
./WeModPatcher/bin/
./WandEnhancer/obj/
./WandEnhancer/bin/
./AsarSharp/obj/
./AsarSharp/bin/
.idea
.idea
packages
*/bin/
*/obj/
*/obj/.nuget/
# App settings (user preferences)
appsettings.json
*DotSettings.user
+30
View File
@@ -0,0 +1,30 @@
INFO ./docs/*
# Wand Enhancer Agent Notes
This repository patches the Wand Electron app from a .NET Framework WPF desktop tool. Keep changes narrow and preserve the patch pipeline invariants.
## Remote Web Panel
- The default local remote port is `3223`. Keep C# and frontend constants aligned.
- The embedded panel must stay small because the desktop patcher embeds it and then injects it into Wand's `app.asar`.
- Production builds must not include mock data, debug routes, sourcemaps, local fonts, heavy icon libraries, or runtime class helper packages.
- Mock/demo data is dev-only and must be reached through `import.meta.env.DEV` dynamic imports.
- Source can use React-compatible imports, but production runtime resolves them to Preact aliases in `web-panel/vite.config.ts`.
- UI uses Tailwind CSS and lightweight shadcn-style local primitives under `web-panel/src/components/ui/`.
- Default renderer scripts live in `web-panel/scripts/default/` and are embedded. Custom user scripts are selected in the WPF patch modal and copied from `PatchConfig.CustomScriptPaths`; only existing `.js` files are accepted. A local `renderer-scripts/` folder next to the patcher exe is still copied as an advanced fallback.
## ASAR Patch Pipeline
- Preserve and restore both `resources/app.asar` and `resources/app.asar.unpacked` backups.
- Inject `web-panel/dist` as `remote-panel/`, `web-panel/bridge/wand-remote-bridge.cjs` as `remote-panel/bridge.cjs`, and default/selected/local renderer scripts under `remote-panel/renderer-scripts`.
- Do not commit extracted `.sources/` output. Recreate it only for reverse-engineering sessions.
- `AsarSharp.AsarExtractor.ExtractAll` must skip unpacked entries when their source path equals the destination (in-place extraction is a self-copy that fails on locked files like `TrainerLib_x64.dll`) and silently skip unpacked entries whose source is missing on disk (e.g. `auxiliary/GameLauncher.exe` removed by an installer). Do not reintroduce hard failure on either case.
- The `DevToolsOnF12` patch anchors on the Electron main-process `<app>.whenReady().then(` site and attaches a `before-input-event` hook to every `BrowserWindow.webContents`. Do not patch the renderer keydown listener — the minified `ACTION_OPEN_DEV_TOOLS` dispatch site is not stable across Wand releases.
- Cheats can be pinned per game in the web panel via `pinned-storage.ts` (`localStorage` key `wand-remote.pinned-cheats.v1:<gameId>`). Pinned cheats render as a virtual `pinned` category at the top of the list; their normal category placement is preserved.
## Validation
- Web panel build: `cd web-panel && pnpm run build`.
- Bridge syntax checks: `node --check web-panel/bridge/wand-remote-bridge.cjs` and `node --check web-panel/scripts/default/remote-popup-cleanup.js`.
- Production dist should contain only static assets and should not contain `mock-instance`, `Mock Adventure`, `Simulation`, `Debug session`, `mock=1`, `demo-session`, `vite.svg`, `tailwind-merge`, `class-variance-authority`, or `clsx`.
+35 -6
View File
@@ -96,12 +96,41 @@ namespace AsarSharp
// it's a file, try to extract it
try
{
byte[] content;
content = Disk.ReadFileSync(filesystem, filename, file);
File.WriteAllBytes(destFilename, content);
// Unpacked entries already live on disk next to the archive in
// "<archive>.unpacked". When the caller extracts INTO that same
// directory (e.g. re-extracting in place to repack later) reading +
// writing the file is a self-copy that needlessly fails when the
// file is locked by another process (TrainerLib_x64.dll) or has been
// removed from disk by an installer (auxiliary/GameLauncher.exe).
if (file.Unpacked == true)
{
string unpackedSourcePath = Path.GetFullPath(
Path.Combine($"{filesystem.GetRootPath()}.unpacked", filename));
string unpackedDestPath = Path.GetFullPath(destFilename);
if (string.Equals(unpackedSourcePath, unpackedDestPath, StringComparison.OrdinalIgnoreCase))
{
// Nothing to do the file is already at the destination.
continue;
}
if (!File.Exists(unpackedSourcePath))
{
// The header references an unpacked file that no longer
// exists on disk; skip it instead of aborting the whole
// extraction so the rest of the asar can still be repacked.
continue;
}
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
File.Copy(unpackedSourcePath, destFilename, true);
}
else
{
byte[] content = Disk.ReadFileSync(filesystem, filename, file);
File.WriteAllBytes(destFilename, content);
}
if (file.Executable == true && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Extensions.SetUnixFilePermission(destFilename, "755");
+111
View File
@@ -0,0 +1,111 @@
# Contributing to WandEnhancer
Thank you for your interest in the WandEnhancer project! This document provides guidelines for contributing to the project.
## Table of Contents
- [Development Environment Setup](#development-environment-setup)
- [Bug Reports](#bug-reports)
- [Feature Suggestions](#feature-suggestions)
- [Creating a Pull Request](#creating-a-pull-request)
- [Code Style](#code-style)
- [Testing](#testing)
- [License](#license)
## Code of Conduct
By participating in this project, you commit to maintaining respectful interactions with all community members. Any form of insults, harassment, or other unacceptable behavior will not be tolerated.
## Project Structure
The project consists of the following main components:
- **WandEnhancer** - Main project containing the enhancement logic and user interface
- **AsarSharp** - Library for working with ASAR archives (used for unpacking and modifying WeMod files)
- **Core** - Core of the enhancement flow, including static and dynamic modifications
- **Models** - Data models used in the project
- **View** - User interface components
## Development Environment Setup
1. Clone the repository:
```
git clone https://github.com/k1tbyte/Wand-Enhancer.git
```
2. Open the solution `Wand-Enhancer.sln` in Visual Studio or JetBrains Rider.
3. Restore NuGet packages.
4. Build the project.
## Bug Reports
If you've found a bug, please create an Issue with a detailed description:
- WandEnhancer version
- WeMod version where the problem occurred
- Detailed steps to reproduce the bug
- Expected and actual behavior
- Screenshots or error logs (if available)
## Feature Suggestions
Suggestions for new features or improvements are welcome! Create an Issue describing your idea, explaining:
- What problem the proposed improvement solves
- How you envision implementing this feature
- Potential alternatives you've considered
## Creating a Pull Request
1. Fork the repository.
2. Create a branch with a descriptive name:
```
git checkout -b feature/feature-name
```
or
```
git checkout -b fix/fix-name
```
3. Make the necessary changes and commit with clear, descriptive messages.
4. Ensure your code follows the project's style.
5. Push the branch to your fork:
```
git push origin your-branch-name
```
6. Create a Pull Request to the main repository.
7. In the Pull Request description, explain the changes made and why they're necessary.
## Code Style
- Use C# naming conventions:
- PascalCase for class, method, and property names
- camelCase for local variables and parameters
- _camelCase for private fields
- Add comments for complex code sections or patching methods
- Follow SOLID and DRY principles
## Testing
Before submitting a Pull Request, ensure that:
1. Your code compiles without errors
2. You've manually tested the functionality
3. The patch works with the current version of WeMod
4. Changes don't break existing functionality
## License
By contributing, you agree that your contributions will be licensed under the [Apache License 2.0](LICENSE.md).
---
Thank you for contributing to the WandEnhancer project!
+39 -30
View File
@@ -3,57 +3,61 @@
![logo](./assets/icon.svg)
---
<h1>WeMod Patcher</h1>
# WandEnhancer
</div>
<h4>WeMod patcher allows you to get some WeMod Pro features absolutely free. This script patches WeMod, thereby removing the daily usage limit (2h), etc.</h4>
<h4>An open-source interoperability tool designed to extend local client-side configurations and improve the UX of the Wand application.</h4>
## 👾 No malware? Is it safe for me?
**🚨 IMPORTANT NOTICE: THIS PROJECT HAS NO OFFICIAL YOUTUBE TUTORIALS OR GUIDES. 🚨
There are no official videos showing how to install or use this tool. Scammers are creating fake tutorials using this project's name and placing malware/password stealers in the video descriptions. If you downloaded an .exe or archive from a YouTube link, YOU HAVE DOWNLOADED MALWARE. The only official, safe, and original source for this project is this exact GitHub repository. We are not responsible for third-party downloads.**
Yes, this is safe. This script is open-source, and you can check the code yourself. I have no intention of harming you or your computer. The script does absolutely nothing with your computer and does not require Internet access. It only affects WeMod files.
## 👾 Is it safe to use?
## 💻 Does this script only work with older versions of WeMod like other unlockers?
Yes. This project is entirely open-source, allowing anyone to audit the code. It operates strictly locally, does not require internet access, and makes zero network requests. It simply adjusts local client settings to enhance your user experience.
With this patch you will be able to use the latest version together with Pro.
## 💫 What features are improved?
## 💫 What features will be available?
✅ Local environment configuration management <br/>
✅ Automated compatibility adjustments for new client versions <br/>
✅ Advanced layout and theme customization (Client-side only) <br/>
✅ AI Features <br/>
✅ Remote web panel (Remote Connect on mobile) <br/>
✅ Unlimited usage time <br/>
✅ Disabling automatic updates (optional) <br/>
✅ Automatic patching of new WeMod versions <br/>
✅ AI Game guides <br/>
✅ Saving mods <br/>
✅ Exclusive to pro subscription customization for hacks <br/>
✅ Hotkeys (hotkey functionality is broken after static patching for unknown reason) <br/>
❌ Connect phone <br/>
## 🌐 Remote Web Panel
WandEnhancer includes a built-in **Remote Web Panel** allowing you to control app features directly from your phone.
### Quick Start:
1. Ensure both your PC and phone are on the **same Wi-Fi network**.
2. Hover over the **Connect** button in the top bar of WandEnhancer.
3. Scan the displayed **QR code** with your phone's camera.
### Troubleshooting & Remote Access:
- **Page isn't loading?** First, ensure both your PC and phone are connected to the **exact same Wi-Fi network**. Next, make sure **Network Discovery** is turned on in your Windows network settings. If it still doesn't work, Windows Firewall might be blocking the connection—you may need to manually allow inbound traffic on TCP port `3223`.
- **Using mobile data or a different network?** If you want to use the panel over mobile data (LTE/5G) or from an entirely different network, you can use [Tailscale](https://tailscale.com/) or similar VPN tools.
## 👀 How to use?
1. Go to [Releases](https://github.com/k1tbyte/Wemod-Patcher/releases) page.
2. Download latest version
3. Run and click the patch
1. Go to the [Releases](https://github.com/k1tbyte/Wand-Enhancer/releases) page.
2. Download the latest source or binary.
3. Run the enhancer to apply local client modifications.
---
## ❓ Q&A
- I applied the patch but when I inject I get stuck on 'Loading mods...'.
- Just close WeMod and try again
- During the game, some hacks are enabled without my input
- This is a bug after the static patch, you have to turn off hotkeys in WeMod settings
- VirusTotal claims that this program is a malware/trojan.
- Perhaps the patcher does have the same signatures as malware (virtual memory patching). But this is a false positive, you can look at the source code or even build the patcher yourself.
- Does this application transfer any data to the Internet from my computer?
- The short answer is NO. This application does not need access to the Internet. The most it does is download updates if you want it to.
- What makes this application better than other patchers?
- All actions related to patches are performed on your computer. No files of unknown origin will be downloaded.
- **I applied the configuration but get stuck on 'Loading...'**
- Just close the application completely and restart it.
- **Does this send data anywhere?**
- No. All operations are strictly offline and local to your machine.
---
## 🖼️ Screenshots
![1](./assets/screenshots/app1.png)
<div align='center'>
![2](./assets/screenshots/app2.png)
</div>
---
## 📜 License
@@ -65,4 +69,9 @@ This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.md) fi
---
[![Star History Chart](https://api.star-history.com/svg?repos=k1tbyte/Wemod-Patcher&type=Date)](https://www.star-history.com/#k1tbyte/Wemod-Patcher&Date)
> **Legal Disclaimer:**
> This project is a third-party enhancement tool intended solely for educational, research, and local interoperability purposes. It does not distribute any proprietary code or bypass server-side validations. All modifications are performed locally to customize the user's interface.
---
[![Star History Chart](https://api.star-history.com/svg?repos=k1tbyte/Wand-Enhancer&type=Date)](https://www.star-history.com/#k1tbyte/Wand-Enhancer&Date)
+1 -1
View File
@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeModPatcher", "WeModPatcher\WeModPatcher.csproj", "{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WandEnhancer", "WandEnhancer\WandEnhancer.csproj", "{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsarSharp", "AsarSharp\AsarSharp.csproj", "{BEAA604A-402A-4387-8903-A53FC913A26E}"
EndProject
@@ -1,11 +1,12 @@
<Application x:Class="WeModPatcher.App"
<Application x:Class="WandEnhancer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WeModPatcher"
xmlns:converters="clr-namespace:WeModPatcher.Converters">
xmlns:local="clr-namespace:WandEnhancer"
xmlns:converters="clr-namespace:WandEnhancer.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Locale/lang.en-US.xaml"/>
<ResourceDictionary Source="Style/ColorScheme.xaml"/>
<ResourceDictionary Source="Style/Styles.xaml"/>
<ResourceDictionary Source="Style/Icons.xaml"/>
@@ -1,11 +1,12 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using WeModPatcher.Core;
using WeModPatcher.View.MainWindow;
using WandEnhancer.Core;
using WandEnhancer.Core.Services;
using WandEnhancer.View.MainWindow;
using MessageBox = System.Windows.Forms.MessageBox;
namespace WeModPatcher
namespace WandEnhancer
{
/// <summary>
/// Interaction logic for App.xaml
@@ -14,6 +15,7 @@ namespace WeModPatcher
{
protected override void OnStartup(StartupEventArgs e)
{
LocalizationManager.Initialize();
this.MainWindow.Show();
}
@@ -1,25 +1,30 @@
using System;
using System.Reflection;
using WeModPatcher.Models;
using WandEnhancer.Models;
namespace WeModPatcher
namespace WandEnhancer
{
public static class Constants
{
public const string RepoName = "Wemod-Patcher";
public const string RepoName = "Wand-Enhancer";
public const string Owner = "k1tbyte";
/*public const string PatchRegistryName = "patchRegistry.json";*/
public static readonly string RepositoryUrl = $"https://github.com/{Owner}/{RepoName}";
public static readonly Version Version;
public static readonly string[] WeModBrandNames = { "Wand", "WeMod" };
public const string AppSettingsFileName = "appsettings.json";
public const string ProxyDllResouceName = "proxydll";
// cmp dword ptr [rdx], 0
// jnz loc_XXXXXXXX
// mov rsi, rdx
public static Signature ExePatchSignature = new Signature(
/*public static Signature ExePatchSignature = new Signature(
"83 3A 00 0F ?? ?? 01 00 00 48 89 D6 48 B8",
4,
new byte[]{ 0x84, 0x17 },
new byte[]{ 0x85, 0x22 }
);
);*/
/*// ...
// test eax, eax (0x85 for r/m16/32/64)
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
namespace WeModPatcher.Converters
namespace WandEnhancer.Converters
{
public abstract class BaseBooleanConverter<T> : IValueConverter
{
@@ -1,6 +1,6 @@
using System.Windows;
namespace WeModPatcher.Converters
namespace WandEnhancer.Converters
{
internal sealed class ToVisibilityConverter : BaseBooleanConverter<Visibility>
{
+440
View File
@@ -0,0 +1,440 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using AsarSharp;
using WandEnhancer.Models;
using WandEnhancer.Utils;
using WandEnhancer.View.MainWindow;
namespace WandEnhancer.Core
{
public class Enhancer
{
private const string ResourcesDirectoryName = "resources";
private const string AppAsarFileName = "app.asar";
private const string AppAsarUnpackedDirectoryName = "app.asar.unpacked";
private const string AppAsarBackupFileName = "app.asar.backup";
private const string AppAsarUnpackedBackupDirectoryName = "app.asar.unpacked.backup";
private const string WebPanelDirectoryName = "web-panel";
private const string WebPanelDistDirectoryName = "dist";
private const string WebPanelBridgeDirectoryName = "bridge";
private const string WebPanelScriptsDirectoryName = "scripts";
private const string DefaultScriptsDirectoryName = "default";
private const string LocalCustomScriptsDirectoryName = "renderer-scripts";
private const string RemotePanelDirectoryName = "remote-panel";
private const string RemoteBridgeSourceFileName = "wand-remote-bridge.cjs";
private const string RemoteBridgeTargetFileName = "bridge.cjs";
private const string RemoteRendererScriptsDirectoryName = "renderer-scripts";
private const string EmbeddedRemotePanelDistPrefix = "remote-panel/dist/";
private const string EmbeddedRemotePanelBridgeResourceName = "remote-panel/bridge.cjs";
private const string EmbeddedRemotePanelDefaultScriptsPrefix = "remote-panel/renderer-scripts/";
private const string JavaScriptFileExtension = ".js";
private const string JavaScriptFileSearchPattern = "*.js";
private const string DuplicateScriptSuffix = ".custom";
private const int FirstDuplicateScriptIndex = 1;
private readonly WeModConfig _weModConfig;
private readonly Action<string, ELogType> _logger;
private readonly PatchConfig _config;
private readonly string _asarPath;
private readonly string _backupPath;
private readonly string _unpackedPath;
private readonly string _unpackedBackupPath;
public Enhancer(WeModConfig weModConfig, Action<string, ELogType> logger, PatchConfig config)
{
_weModConfig = weModConfig;
_logger = logger;
_config = config;
_asarPath = Path.Combine(weModConfig.RootDirectory, ResourcesDirectoryName, AppAsarFileName);
_unpackedPath = Path.Combine(weModConfig.RootDirectory, ResourcesDirectoryName, AppAsarUnpackedDirectoryName);
_backupPath = Path.Combine(weModConfig.RootDirectory, ResourcesDirectoryName, AppAsarBackupFileName);
_unpackedBackupPath = Path.Combine(weModConfig.RootDirectory, ResourcesDirectoryName, AppAsarUnpackedBackupDirectoryName);
}
private string ApplyJsPatch(string fileName, string js, EnhancerConfig.PatchEntry patch, EPatchType patchType)
{
if (patch.Applied)
{
return js;
}
var matches = patch.Target.Matches(js);
if (matches.Count == 0)
{
return js;
}
var prefix = $"[ENHANCER] [{patchType} -> {patch.Name}]";
if(matches.Count > 1 && patch.SingleMatch)
{
throw new Exception(
$"{prefix} Patch failed. Multiple target functions found. Looks like the version is not supported");
}
if (patch.Resolver != null)
{
string resolvedField = patch.Resolver.Handler(matches[0].Value);
if (string.IsNullOrEmpty(resolvedField))
{
throw new Exception($"{prefix} Resolver failed to find field name");
}
patch.Patch = patch.Patch.Replace(patch.Resolver.Placeholder, resolvedField);
}
_logger($"{prefix} Found target function in: " + Path.GetFileName(fileName), ELogType.Info);
string newJs = patch.Target.Replace(js, patch.Patch);
File.WriteAllText(fileName, newJs);
_logger($"{prefix} Patch applied", ELogType.Success);
patch.Applied = true;
return newJs;
}
private void PatchAsar()
{
var items = Directory.EnumerateFiles(_unpackedPath)
.Where(file => !Directory.Exists(file) && Regex.IsMatch(Path.GetFileName(file), @"^app-\w+|index\.js"))
.ToList();
if (!items.Any())
{
throw new Exception("[ENHANCER] No app bundle found");
}
var remainingPatches = new HashSet<EPatchType>(_config.PatchTypes);
var enhancerConfig = EnhancerConfig.GetInstance();
foreach (var item in items)
{
if (remainingPatches.Count == 0)
{
break;
}
string data = File.ReadAllText(item);
foreach (var entry in remainingPatches.ToList())
{
var entries = enhancerConfig[entry];
foreach (var patchEntry in entries)
{
data = ApplyJsPatch(item, data, patchEntry, entry);
}
if (entries.All(x => x.Applied))
{
remainingPatches.Remove(entry);
}
}
}
if(remainingPatches.Count > 0)
{
var failedPatches = string.Join(", ", remainingPatches.Select(p => p.ToString()));
throw new Exception($"[ENHANCER] Failed to apply patches: {failedPatches}. The version may not be supported.");
}
}
private static string FindWorkspacePath(params string[] segments)
{
string current = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
while (!string.IsNullOrEmpty(current))
{
string candidate = Path.Combine(new[] { current }.Concat(segments).ToArray());
if (Directory.Exists(candidate) || File.Exists(candidate))
{
return candidate;
}
current = Directory.GetParent(current)?.FullName;
}
throw new FileNotFoundException($"Required workspace artifact not found: {Path.Combine(segments)}");
}
private static void CopyDirectory(string sourceDir, string destinationDir)
{
Directory.CreateDirectory(destinationDir);
foreach (var directory in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
{
var relativePath = directory.Substring(sourceDir.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
Directory.CreateDirectory(Path.Combine(destinationDir, relativePath));
}
foreach (var file in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories))
{
var relativePath = file.Substring(sourceDir.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var destinationPath = Path.Combine(destinationDir, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? destinationDir);
File.Copy(file, destinationPath, true);
}
}
private static int CopyJavaScriptFiles(string sourceDir, string destinationDir)
{
if (string.IsNullOrEmpty(sourceDir) || !Directory.Exists(sourceDir))
{
return 0;
}
Directory.CreateDirectory(destinationDir);
int copied = 0;
foreach (var file in Directory.GetFiles(sourceDir, JavaScriptFileSearchPattern, SearchOption.TopDirectoryOnly))
{
File.Copy(file, GetAvailableScriptPath(destinationDir, Path.GetFileName(file)));
copied++;
}
return copied;
}
private static string GetAvailableScriptPath(string destinationDir, string fileName)
{
string destinationPath = Path.Combine(destinationDir, fileName);
if (!File.Exists(destinationPath))
{
return destinationPath;
}
string name = Path.GetFileNameWithoutExtension(fileName);
string extension = Path.GetExtension(fileName);
for (int index = FirstDuplicateScriptIndex; ; index++)
{
destinationPath = Path.Combine(destinationDir, $"{name}{DuplicateScriptSuffix}{index}{extension}");
if (!File.Exists(destinationPath))
{
return destinationPath;
}
}
}
private static int CopyEmbeddedDirectory(string resourcePrefix, string destinationDir)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceNames = assembly.GetManifestResourceNames()
.Where(name => name.StartsWith(resourcePrefix, StringComparison.Ordinal))
.ToList();
if (resourceNames.Count == 0)
{
return 0;
}
Directory.CreateDirectory(destinationDir);
foreach (var resourceName in resourceNames)
{
var relativePath = resourceName.Substring(resourcePrefix.Length)
.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar);
var destinationPath = Path.Combine(destinationDir, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? destinationDir);
using (var resource = assembly.GetManifestResourceStream(resourceName))
{
if (resource == null)
{
throw new FileNotFoundException($"Embedded resource not found: {resourceName}");
}
using (var output = File.Create(destinationPath))
{
resource.CopyTo(output);
}
}
}
return resourceNames.Count;
}
private static bool CopyEmbeddedFile(string resourceName, string destinationPath)
{
var assembly = Assembly.GetExecutingAssembly();
using (var resource = assembly.GetManifestResourceStream(resourceName))
{
if (resource == null)
{
return false;
}
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? ".");
using (var output = File.Create(destinationPath))
{
resource.CopyTo(output);
}
}
return true;
}
private static string FindLocalCustomScriptsPath()
{
string executableDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (string.IsNullOrEmpty(executableDirectory))
{
return null;
}
string localScripts = Path.Combine(executableDirectory, LocalCustomScriptsDirectoryName);
return Directory.Exists(localScripts) ? localScripts : null;
}
private static int CopySelectedJavaScriptFiles(IEnumerable<string> files, string destinationDir)
{
if (files == null)
{
return 0;
}
Directory.CreateDirectory(destinationDir);
int copied = 0;
foreach (var file in files.Where(IsJavaScriptFile).Distinct(StringComparer.OrdinalIgnoreCase))
{
File.Copy(file, GetAvailableScriptPath(destinationDir, Path.GetFileName(file)));
copied++;
}
return copied;
}
private static bool IsJavaScriptFile(string file)
{
return File.Exists(file) && string.Equals(Path.GetExtension(file), JavaScriptFileExtension, StringComparison.OrdinalIgnoreCase);
}
private void InjectRemotePanelFiles()
{
if (!_config.PatchTypes.Contains(EPatchType.RemoteWebPanelPreview))
{
return;
}
string localCustomScriptsRoot = FindLocalCustomScriptsPath();
string targetRoot = Path.Combine(_unpackedPath, RemotePanelDirectoryName);
string targetScriptsRoot = Path.Combine(targetRoot, RemoteRendererScriptsDirectoryName);
string targetBridgePath = Path.Combine(targetRoot, RemoteBridgeTargetFileName);
if (Directory.Exists(targetRoot))
{
Directory.Delete(targetRoot, true);
}
if (CopyEmbeddedDirectory(EmbeddedRemotePanelDistPrefix, targetRoot) == 0)
{
CopyDirectory(FindWorkspacePath(WebPanelDirectoryName, WebPanelDistDirectoryName), targetRoot);
}
if (!CopyEmbeddedFile(EmbeddedRemotePanelBridgeResourceName, targetBridgePath))
{
File.Copy(FindWorkspacePath(WebPanelDirectoryName, WebPanelBridgeDirectoryName, RemoteBridgeSourceFileName), targetBridgePath, true);
}
int defaultScriptCount = CopyEmbeddedDirectory(EmbeddedRemotePanelDefaultScriptsPrefix, targetScriptsRoot);
if (defaultScriptCount == 0)
{
defaultScriptCount = CopyJavaScriptFiles(FindWorkspacePath(WebPanelDirectoryName, WebPanelScriptsDirectoryName, DefaultScriptsDirectoryName), targetScriptsRoot);
}
int selectedScriptCount = CopySelectedJavaScriptFiles(_config.CustomScriptPaths, targetScriptsRoot);
int localScriptCount = CopyJavaScriptFiles(localCustomScriptsRoot, targetScriptsRoot);
_logger($"[ENHANCER] Injected remote panel assets and renderer scripts into app.asar (default: {defaultScriptCount}, selected: {selectedScriptCount}, local: {localScriptCount})", ELogType.Info);
}
private void AttachProxyDll()
{
var assembly = Assembly.GetExecutingAssembly();
var dll = assembly.GetManifestResourceStream(Constants.ProxyDllResouceName);
if (dll == null)
{
throw new Exception("[ENHANCER] Proxy DLL resource not found");
}
var destPath = Path.Combine(_weModConfig.RootDirectory, "version.dll");
using (var fileStream = File.Create(destPath))
{
dll.CopyTo(fileStream);
}
_logger("[ENHANCER] Proxy DLL attached", ELogType.Info);
}
public void Patch()
{
Common.TryKillProcess(_weModConfig.BrandName);
if (!File.Exists(_backupPath))
{
_logger("[ENHANCER] Creating backup...", ELogType.Info);
File.Copy(_asarPath, _backupPath);
}
else
{
_logger("[ENHANCER] Backup found, restoring pristine app.asar before patching...", ELogType.Info);
File.Copy(_backupPath, _asarPath, true);
}
if (!Directory.Exists(_unpackedBackupPath) && Directory.Exists(_unpackedPath))
{
_logger("[ENHANCER] Creating backup of app.asar.unpacked...", ELogType.Info);
CopyDirectory(_unpackedPath, _unpackedBackupPath);
}
else if (Directory.Exists(_unpackedBackupPath))
{
_logger("[ENHANCER] Restoring pristine app.asar.unpacked before patching...", ELogType.Info);
if (Directory.Exists(_unpackedPath))
{
Directory.Delete(_unpackedPath, true);
}
CopyDirectory(_unpackedBackupPath, _unpackedPath);
}
else if (!Directory.Exists(_unpackedPath))
{
throw new Exception("[ENHANCER] app.asar.unpacked is missing and no backup exists. Restore the original Wand installation files or reinstall Wand, then patch again.");
}
if(!File.Exists(_asarPath))
{
throw new Exception("app.asar not found");
}
try
{
_logger("[ENHANCER] Extracting app.asar...", ELogType.Info);
AsarExtractor.ExtractAll(_asarPath, _unpackedPath);
}
catch (Exception e)
{
throw new Exception($"[ENHANCER] Failed to unpack app.asar: {e.Message}");
}
PatchAsar();
InjectRemotePanelFiles();
try
{
new AsarCreator(_unpackedPath, _asarPath, new CreateOptions
{
Unpack = new Regex(@"^static\\unpacked.*$")
}).CreatePackageWithOptions();
}
catch (Exception e)
{
throw new Exception($"[ENHANCER] Failed to pack app.asar: {e.Message}");
}
AttachProxyDll();
_logger("[ENHANCER] Done!", ELogType.Success);
}
}
}
+165
View File
@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using WandEnhancer.Models;
namespace WandEnhancer.Core
{
public static class EnhancerConfig
{
private const int RemoteWebPanelDefaultPort = 3223;
private static readonly string RemoteWebPanelFallbackUrl = $"http://localhost:{RemoteWebPanelDefaultPort}/remote/";
public class ResolveContext
{
public string Placeholder { get; set; }
public Func<string, string> Handler { get; set; }
}
public class PatchEntry
{
public Regex Target { get; set; }
public string Patch { get; set; }
public string Name { get; set; }
public bool Applied { get; set; }
public bool SingleMatch { get; set; } = true;
public ResolveContext Resolver { get; set; }
}
public static Dictionary<EPatchType, PatchEntry[]> GetInstance()
{
return new Dictionary<EPatchType, PatchEntry[]>()
{
{
EPatchType.ActivatePro,
new[]
{
new PatchEntry
{
Resolver = new ResolveContext
{
Handler = (targetFunction) =>
{
var fetchMatch = Regex.Match(targetFunction, @"return\s+this\.#(\w+)\.fetch");
return fetchMatch.Success ? fetchMatch.Groups[1].Value : null;
},
Placeholder = "<service_name>"
},
Name = "getUserAccount",
Target = new Regex(@"getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?\}\)\}",
RegexOptions.Singleline),
Patch =
"getUserAccount(){return this.#<service_name>.fetch({endpoint:\"/v3/account\",method:\"GET\",name:\"/v3/account\",collectMetrics:0}).then(response=>{response.subscription={period:\"yearly\",state:\"active\"};return response;})}"
},
new PatchEntry
{
Resolver = new ResolveContext
{
Handler = (targetFunction) =>
{
var match = Regex.Match(targetFunction, @"return\s+this\.#(\w+)\.post");
return match.Success ? match.Groups[1].Value : null;
},
Placeholder = "<service_name>"
},
Name = "setAccountWandBrandExperience",
Target = new Regex(
@"setAccountWandBrandExperience\(\){.*?return\s+this\.#\w+\.post\(""/v3/account/brand_experience_wand""\)}",
RegexOptions.Singleline),
Patch =
"setAccountWandBrandExperience(){return this.#<service_name>.post(\"/v3/account/brand_experience_wand\").then(response=>{response.subscription={period:\"yearly\",state:\"active\"};return response;})}"
}
}
},
{
EPatchType.DisableUpdates,
new[]
{
new PatchEntry
{
Target = new Regex(@"registerHandler\(""ACTION_CHECK_FOR_UPDATE"".*?\)\)\)\)",
RegexOptions.Singleline),
Patch = "registerHandler(\"ACTION_CHECK_FOR_UPDATE\",(e=>expectUpdateFeedUrl(e,(e=>null)))"
}
}
},
{
EPatchType.DevToolsOnF12,
new[]
{
new PatchEntry
{
Name = "devToolsBeforeInputEvent",
// Anchor on the Electron main-process `<app>.whenReady().then(`
// call. This site is far more stable than the minified renderer
// keydown listener that previously held the F12 -> ACTION_OPEN_DEV_TOOLS
// dispatch (its identifiers and shape change on every Wand release).
// We attach a `before-input-event` hook to every BrowserWindow's
// webContents which toggles DevTools on F12 directly from the main
// process, bypassing the renderer dispatcher entirely.
Target = new Regex(@"(?<app>\w+)\.whenReady\(\)\.then\("),
Patch = "${app}.on(\"browser-window-created\",((_,w)=>{try{w.webContents.on(\"before-input-event\",((_,i)=>{if(\"F12\"===i.key&&\"keyDown\"===i.type){w.webContents.isDevToolsOpened()?w.webContents.closeDevTools():w.webContents.openDevTools({mode:\"detach\"})}}))}catch(e){}})),${app}.whenReady().then("
}
}
},
{
EPatchType.RemoteWebPanelPreview,
new[]
{
new PatchEntry
{
Name = "remoteBridgeMainBoot",
Target = new Regex(@"(?<app>\w+)\.whenReady\(\)\.then\(run\)"),
Patch = "${app}.whenReady().then(()=>{try{const p=require(\"node:path\");require(p.join(__dirname,\"remote-panel\",\"bridge.cjs\")).installWandRuntime(require(\"electron\"));}catch(e){try{const fs=require(\"node:fs\"),os=require(\"node:os\"),p=require(\"node:path\");fs.appendFileSync(p.join(os.tmpdir(),\"wand-remote-bridge.log\"),\"[\"+new Date().toISOString()+\"] [boot-error] \"+(e&&e.stack||e)+\"\\n\");}catch(_){}}return run()})"
},
new PatchEntry
{
Name = "remoteBridgeReset",
Target = new Regex(@"#Je\(\)\{this\.#Oe&&\(this\.#Oe\.dispose\(\),this\.#Oe=null\),this\.#Pe=Date\.now\(\)\.toString\(\),this\.#ke=null,this\.#_e=\[],this\.#Ee=null\}"),
Patch = "#Je(){this.#Oe&&(this.#Oe.dispose(),this.#Oe=null),this.#Pe=Date.now().toString(),this.#ke=null,this.#_e=[],this.#Ee=null,this.__wandRemoteTrainerInfo=null,this.__wandRemoteBridge?.sync(null)}"
},
new PatchEntry
{
Name = "remoteBridgeSyncSnapshot",
Target = new Regex(@"#Be\(\)\{if\(this\.status===i\.Connected\)\{let e,t=!1,s=this\.#Ee\?\.getMetadata\(h\.vO\)\?\.gameVersion\?\?null,i=!1;const n=this\.#Ve\[this\.#ke\?\?""""\]\|\|null;this\.#Re&&\(e=this\.#Ae\.getPreferredInstallationInfo\(this\.#Re\),e\.app&&\(t=!0,s\?\?=e\.version\?\?null,i=""number""==typeof e\.version&&!this\.#_e\.includes\(e\.version\)\)\),this\.#Me\?\.send\(""client-state"",\{instanceId:this\.#Pe,trainerId:this\.#ke,trainerLoading:this\.#Ee\?\.isLoading\(\),gameInstalled:t,gameVersion:s,needsCompatibilityWarning:i,values:this\.#Ke\(\),themeId:this\.#We,settings:R\(this\.settings\),language:this\.#Ne,accountUuid:this\.account\.uuid,notesReadHash:n,isTimeLimitExpired:""expired""===this\.#Fe\.timerState\}\)\}\}"),
Patch = "#Be(){let e,t=!1,s=this.#Ee?.getMetadata(h.vO)?.gameVersion??null,o=!1;const n=this.#Ve[this.#ke??\"\"]||null;this.#Re&&(e=this.#Ae.getPreferredInstallationInfo(this.#Re),e.app&&(t=!0,s??=e.version??null,o=\"number\"==typeof e.version&&!this.#_e.includes(e.version)));this.status===i.Connected&&this.#Me?.send(\"client-state\",{instanceId:this.#Pe,trainerId:this.#ke,trainerLoading:this.#Ee?.isLoading(),gameInstalled:t,gameVersion:s,needsCompatibilityWarning:o,values:this.#Ke(),themeId:this.#We,settings:R(this.settings),language:this.#Ne,accountUuid:this.account.uuid,notesReadHash:n,isTimeLimitExpired:\"expired\"===this.#Fe.timerState});this.__wandRemoteBridge?.sync({instanceId:this.#Pe,trainerId:this.#ke,trainerInfo:this.__wandRemoteTrainerInfo??null,metadata:this.#Ee?.getMetadata(h.vO)??null,trainerLoading:this.#Ee?.isLoading()??false,gameInstalled:t,gameVersion:s,needsCompatibilityWarning:o,language:this.#Ne,themeId:this.#We,notesReadHash:n,isTimeLimitExpired:\"expired\"===this.#Fe.timerState,values:this.#Ke()})}"
},
new PatchEntry
{
Name = "remoteBridgeBindHandler",
Target = new Regex(@"setCurrentTrainer\(e,t=null\)\{const s=e\?\.trainerId\|\|null,i=\(s\?e\?\.gameId:null\)\|\|null,n=\(s\?e\?\.supportedVersions:null\)\|\|\[];if\(s===this\.#ke&&t===this\.#Ee\)return;"),
Patch = "setCurrentTrainer(e,t=null){this.__wandRemoteBridge||(this.__wandRemoteBridge=(()=>{try{const r=globalThis.require||require;const{ipcRenderer:c}=r(\"electron\");try{c.invoke(\"wand-remote-url\").then((u=>{u&&(globalThis.__wandRemoteBridgeUrl=u)}))}catch(e){}const send=(ch,p)=>{try{return c.invoke(ch,p&&JSON.parse(JSON.stringify(p)))}catch(e){}};return{sync:(s)=>send(\"wand-remote-sync\",s),valueChanged:(s)=>send(\"wand-remote-value-changed\",s),setHandler:(h)=>{if(this.__wandRemoteBridgeBound)return;this.__wandRemoteBridgeBound=true;try{c.invoke(\"wand-remote-set-handler-bind\")}catch(e){}c.on(\"wand-remote-set-value\",(_e,req)=>{try{h(req)}catch(e){}})}}}catch(e){try{const r=globalThis.require||require,fs=r(\"node:fs\"),os=r(\"node:os\"),p=r(\"node:path\");fs.appendFileSync(p.join(os.tmpdir(),\"wand-remote-bridge.log\"),\"[\"+new Date().toISOString()+\"] [renderer-bind-error] \"+(e&&e.stack||e)+\"\\n\");}catch(_){}return null}})());this.__wandRemoteBridge?.setHandler((e=>{if(!this.#Ee||!e?.target)return!1;return this.#Ee.isActive()?this.#Ee.setValue(e.target,e.value,g.kL.Remote,e.cheatId):!1}));this.__wandRemoteTrainerInfo=e??null;const s=e?.trainerId||null,i=(s?e?.gameId:null)||null,n=(s?e?.supportedVersions:null)||[];if(s===this.#ke&&t===this.#Ee)return;"
},
new PatchEntry
{
Name = "remoteBridgeValueDelta",
Target = new Regex(@"#ct\(e,t\)\{t\.push\(e\.onValueSet\(e=>\{this\.status===i\.Connected&&e\.source!==g\.kL\.Remote&&this\.#Me\?\.send\(""client-value-changed"",\{instanceId:this\.#Pe,name:e\.name,value:e\.value,cheatId:e\.cheatId\}\)\}\)\),this\.#Be\(\)\}"),
Patch = "#ct(e,t){t.push(e.onValueSet(e=>{this.status===i.Connected&&e.source!==g.kL.Remote&&this.#Me?.send(\"client-value-changed\",{instanceId:this.#Pe,name:e.name,value:e.value,cheatId:e.cheatId}),this.__wandRemoteBridge?.valueChanged({trainerId:this.#ke,target:e.name,value:e.value,oldValue:e.oldValue,source:String(e.source??\"desktop\"),cheatId:e.cheatId})})),this.#Be()}"
},
new PatchEntry
{
Name = "remoteTooltipPreviewUrl",
Target = new Regex(@"remoteUrl=""wemodwebsite://remote"""),
Patch = "remoteUrl=globalThis.__wandRemoteBridgeUrl||\"" + RemoteWebPanelFallbackUrl + "\""
},
new PatchEntry
{
Name = "remoteQrPreviewUrl",
Resolver = new ResolveContext
{
Handler = (matchContent) =>
{
var match = Regex.Match(matchContent, @"this\.canvasElement&&(\w+)\.mo");
return match.Success ? match.Groups[1].Value : null;
},
Placeholder = "<qr_writer>"
},
Target = new Regex(@"this\.canvasElement&&\w+\.mo\(this\.canvasElement,`\$\{\w+\.A\.wemodWebsiteUrl\}/remote`,this\.options\)"),
Patch = "this.canvasElement&&<qr_writer>.mo(this.canvasElement,globalThis.__wandRemoteBridgeUrl||\"" + RemoteWebPanelFallbackUrl + "\",this.options)"
}
}
}
};
}
}
}
@@ -0,0 +1,150 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Windows;
namespace WandEnhancer.Core.Services
{
public static class LocalizationManager
{
public static readonly List<CultureInfo> SupportedLanguages = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("zh-CN"),
new CultureInfo("de-DE"),
new CultureInfo("fr-FR"),
new CultureInfo("es-ES"),
new CultureInfo("it-IT"),
new CultureInfo("pt-BR"),
new CultureInfo("pl-PL"),
new CultureInfo("ru-RU"),
new CultureInfo("uk-UA"),
new CultureInfo("ja-JP"),
new CultureInfo("tr-TR")
};
private static CultureInfo _currentLanguage;
private static ResourceDictionary _englishBaseDictionary;
public static CultureInfo CurrentLanguage
{
get => _currentLanguage;
set => SetLanguage(value);
}
public static void Initialize()
{
// Load English as the base fallback dictionary
_englishBaseDictionary = new ResourceDictionary
{
Source = new Uri("Locale/lang.en-US.xaml", UriKind.Relative)
};
// Try to load saved language preference
var savedLanguage = SettingsManager.LoadSettings()?.Language;
CultureInfo targetCulture = null;
if (!string.IsNullOrEmpty(savedLanguage))
{
targetCulture = SupportedLanguages.FirstOrDefault(c => c.Name == savedLanguage);
}
if (targetCulture == null)
{
// Fall back to system culture detection
var systemCulture = Thread.CurrentThread.CurrentUICulture;
targetCulture = SupportedLanguages.FirstOrDefault(c =>
c.Name == systemCulture.Name ||
c.TwoLetterISOLanguageName == systemCulture.TwoLetterISOLanguageName);
}
SetLanguage(targetCulture ?? SupportedLanguages[0], saveSettings: false);
}
private static void SetLanguage(CultureInfo culture, bool saveSettings = true)
{
if (culture == null)
throw new ArgumentNullException(nameof(culture));
if (Equals(culture, _currentLanguage))
return;
var supportedCulture = SupportedLanguages.FirstOrDefault(c => c.Name == culture.Name);
if (supportedCulture == null)
{
supportedCulture = SupportedLanguages[0]; // Default to English
}
_currentLanguage = supportedCulture;
Thread.CurrentThread.CurrentUICulture = supportedCulture;
// Create the locale dictionary with English as base for fallback
var localeDict = new ResourceDictionary();
// First, add English base dictionary for fallback
if (_englishBaseDictionary != null && supportedCulture.Name != SupportedLanguages[0].Name)
{
foreach (var key in _englishBaseDictionary.Keys)
{
localeDict[key] = _englishBaseDictionary[key];
}
}
// Then overlay with the selected language (will override English keys)
var targetDict = new ResourceDictionary
{
Source = new Uri($"Locale/lang.{supportedCulture.Name}.xaml", UriKind.Relative)
};
foreach (DictionaryEntry entry in targetDict)
{
localeDict[entry.Key] = targetDict[entry.Key];
}
// Find and replace the old locale dictionary
var oldDict = Application.Current.Resources.MergedDictionaries
.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.StartsWith("Locale/lang."));
if (oldDict != null)
{
var index = Application.Current.Resources.MergedDictionaries.IndexOf(oldDict);
Application.Current.Resources.MergedDictionaries.Remove(oldDict);
Application.Current.Resources.MergedDictionaries.Insert(index, localeDict);
}
else
{
Application.Current.Resources.MergedDictionaries.Add(localeDict);
}
if (saveSettings)
{
SettingsManager.SaveSettings(new AppSettings { Language = supportedCulture.Name });
}
}
public static string GetLanguageDisplayName(CultureInfo culture)
{
try
{
var dict = new ResourceDictionary
{
Source = new Uri($"Locale/lang.{culture.Name}.xaml", UriKind.Relative)
};
if (dict.Contains("language_display_name"))
{
return dict["language_display_name"] as string ?? culture.NativeName;
}
}
catch
{
// Fallback to native name if loading fails
}
return culture.NativeName;
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace WandEnhancer.Core.Services
{
public class AppSettings
{
public string Language { get; set; }
}
public static class SettingsManager
{
private static readonly string SettingsPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
Constants.AppSettingsFileName);
public static AppSettings LoadSettings()
{
try
{
if (File.Exists(SettingsPath))
{
var json = File.ReadAllText(SettingsPath);
return JsonConvert.DeserializeObject<AppSettings>(json);
}
}
catch (Exception)
{
// Settings loading is non-critical - silently fall back to defaults
// This can fail due to file permissions, corrupted JSON, etc.
}
return null;
}
public static void SaveSettings(AppSettings settings)
{
try
{
var json = JsonConvert.SerializeObject(settings, Formatting.Indented);
File.WriteAllText(SettingsPath, json);
}
catch (Exception)
{
// Settings saving is non-critical - silently ignore errors
// This can fail due to file permissions or read-only directories
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Deutsch</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Eine neue Version ist verfügbar</s:String>
<s:String x:Key="mw_folder_path">Ordnerpfad</s:String>
<s:String x:Key="mw_folder_not_found">Ordner nicht gefunden</s:String>
<s:String x:Key="mw_patch">Anwenden</s:String>
<s:String x:Key="mw_restore">Wiederherstellen</s:String>
<s:String x:Key="mw_source_code">Quellcode</s:String>
<s:String x:Key="mw_made_by">Mit ❤️ von k1tbyte erstellt</s:String>
<s:String x:Key="mw_star_hint">Gib einen Stern, wenn dir das geholfen hat ;)</s:String>
<s:String x:Key="mw_copy_logs">Logs in die Zwischenablage kopieren</s:String>
<s:String x:Key="mw_export_logs">Logs in Datei exportieren</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Einstellungen</s:String>
<s:String x:Key="settings_language">Sprache</s:String>
<s:String x:Key="settings_save">Speichern</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">WeMod Pro aktivieren</s:String>
<s:String x:Key="pv_devtools">DevTools mit F12</s:String>
<s:String x:Key="pv_disable_updates">Updates deaktivieren</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Remote-Zugriff aktivieren (Beta)</s:String>
<s:String x:Key="pv_custom_scripts">Benutzerdefinierte Skripte</s:String>
<s:String x:Key="pv_add_js_scripts">.js hinzufügen</s:String>
<s:String x:Key="pv_custom_scripts_hint">Ausgewählte .js-Dateien werden in Wand gepackt und im Renderer geladen.</s:String>
<s:String x:Key="pv_no_custom_scripts">Keine Skripte ausgewählt</s:String>
<s:String x:Key="pv_start">Starten</s:String>
<s:String x:Key="pv_popup_title">Was werden wir verbessern?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Vor dem Update wird dringend empfohlen, Änderungen rückgängig zu machen, falls sie angewendet wurden</s:String>
<s:String x:Key="up_update_now">Jetzt aktualisieren</s:String>
<s:String x:Key="up_popup_title">Update verfügbar!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">English</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">A new version is available</s:String>
<s:String x:Key="mw_folder_path">Folder path</s:String>
<s:String x:Key="mw_folder_not_found">Folder not found</s:String>
<s:String x:Key="mw_patch">Enhance</s:String>
<s:String x:Key="mw_restore">Restore</s:String>
<s:String x:Key="mw_source_code">Source code</s:String>
<s:String x:Key="mw_made_by">Made with ❤️ by k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Put a star if you found this helpful ;)</s:String>
<s:String x:Key="mw_copy_logs">Copy logs to clipboard</s:String>
<s:String x:Key="mw_export_logs">Export logs to file</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Settings</s:String>
<s:String x:Key="settings_language">Language</s:String>
<s:String x:Key="settings_save">Save</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Activate WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools on F12</s:String>
<s:String x:Key="pv_disable_updates">Disable updates</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Enable remote access (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Custom scripts</s:String>
<s:String x:Key="pv_add_js_scripts">Add .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Selected .js files are packed into Wand and loaded in the renderer.</s:String>
<s:String x:Key="pv_no_custom_scripts">No scripts selected</s:String>
<s:String x:Key="pv_start">Start</s:String>
<s:String x:Key="pv_popup_title">What are we gonna enhance?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Before updating, it is strongly recommended to roll back modifications if they have been applied</s:String>
<s:String x:Key="up_update_now">Update now</s:String>
<s:String x:Key="up_popup_title">Update available!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Español</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Una nueva versión está disponible</s:String>
<s:String x:Key="mw_folder_path">Ruta de la carpeta</s:String>
<s:String x:Key="mw_folder_not_found">Carpeta no encontrada</s:String>
<s:String x:Key="mw_patch">Aplicar</s:String>
<s:String x:Key="mw_restore">Restaurar</s:String>
<s:String x:Key="mw_source_code">Código fuente</s:String>
<s:String x:Key="mw_made_by">Hecho con ❤️ por k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Pon una estrella si te fue útil ;)</s:String>
<s:String x:Key="mw_copy_logs">Copiar registros al portapapeles</s:String>
<s:String x:Key="mw_export_logs">Exportar registros a un archivo</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Configuración</s:String>
<s:String x:Key="settings_language">Idioma</s:String>
<s:String x:Key="settings_save">Guardar</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Activar WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools en F12</s:String>
<s:String x:Key="pv_disable_updates">Desactivar actualizaciones</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Habilitar acceso remoto (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Scripts personalizados</s:String>
<s:String x:Key="pv_add_js_scripts">Agregar .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Los archivos .js seleccionados se empaquetan en Wand y se cargan en el renderer.</s:String>
<s:String x:Key="pv_no_custom_scripts">No hay scripts seleccionados</s:String>
<s:String x:Key="pv_start">Iniciar</s:String>
<s:String x:Key="pv_popup_title">¿Qué vamos a mejorar?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Antes de actualizar, se recomienda encarecidamente revertir las modificaciones si se han aplicado</s:String>
<s:String x:Key="up_update_now">Actualizar ahora</s:String>
<s:String x:Key="up_popup_title">¡Actualización disponible!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Français</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Une nouvelle version est disponible</s:String>
<s:String x:Key="mw_folder_path">Chemin du dossier</s:String>
<s:String x:Key="mw_folder_not_found">Dossier non trouvé</s:String>
<s:String x:Key="mw_patch">Appliquer</s:String>
<s:String x:Key="mw_restore">Restaurer</s:String>
<s:String x:Key="mw_source_code">Code source</s:String>
<s:String x:Key="mw_made_by">Fait avec ❤️ par k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Mettez une étoile si cela vous a aidé ;)</s:String>
<s:String x:Key="mw_copy_logs">Copier les logs dans le presse-papiers</s:String>
<s:String x:Key="mw_export_logs">Exporter les logs dans un fichier</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Paramètres</s:String>
<s:String x:Key="settings_language">Langue</s:String>
<s:String x:Key="settings_save">Enregistrer</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Activer WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools sur F12</s:String>
<s:String x:Key="pv_disable_updates">Désactiver les mises à jour</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Activer l'accès à distance (bêta)</s:String>
<s:String x:Key="pv_custom_scripts">Scripts personnalisés</s:String>
<s:String x:Key="pv_add_js_scripts">Ajouter .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Les fichiers .js sélectionnés sont intégrés dans Wand et chargés dans le renderer.</s:String>
<s:String x:Key="pv_no_custom_scripts">Aucun script sélectionné</s:String>
<s:String x:Key="pv_start">Démarrer</s:String>
<s:String x:Key="pv_popup_title">Qu'allons-nous modifier ?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Avant la mise à jour, il est fortement recommandé d'annuler les modifications si elles ont été appliquées</s:String>
<s:String x:Key="up_update_now">Mettre à jour maintenant</s:String>
<s:String x:Key="up_popup_title">Mise à jour disponible !</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Italiano</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">È disponibile una nuova versione</s:String>
<s:String x:Key="mw_folder_path">Percorso cartella</s:String>
<s:String x:Key="mw_folder_not_found">Cartella non trovata</s:String>
<s:String x:Key="mw_patch">Applica</s:String>
<s:String x:Key="mw_restore">Ripristina</s:String>
<s:String x:Key="mw_source_code">Codice sorgente</s:String>
<s:String x:Key="mw_made_by">Creato con ❤️ da k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Metti una stella se ti è stato utile ;)</s:String>
<s:String x:Key="mw_copy_logs">Copia i log negli appunti</s:String>
<s:String x:Key="mw_export_logs">Esporta i log su file</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Impostazioni</s:String>
<s:String x:Key="settings_language">Lingua</s:String>
<s:String x:Key="settings_save">Salva</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Attiva WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools su F12</s:String>
<s:String x:Key="pv_disable_updates">Disattiva aggiornamenti</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Abilita accesso remoto (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Script personalizzati</s:String>
<s:String x:Key="pv_add_js_scripts">Aggiungi .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">I file .js selezionati vengono inseriti in Wand e caricati nel renderer.</s:String>
<s:String x:Key="pv_no_custom_scripts">Nessuno script selezionato</s:String>
<s:String x:Key="pv_start">Avvia</s:String>
<s:String x:Key="pv_popup_title">Cosa modificheremo?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Prima dell'aggiornamento, si consiglia vivamente di annullare le modifiche se sono state applicate</s:String>
<s:String x:Key="up_update_now">Aggiorna ora</s:String>
<s:String x:Key="up_popup_title">Aggiornamento disponibile!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">日本語</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">新しいバージョンが利用可能です</s:String>
<s:String x:Key="mw_folder_path">フォルダパス</s:String>
<s:String x:Key="mw_folder_not_found">フォルダが見つかりません</s:String>
<s:String x:Key="mw_patch">適用</s:String>
<s:String x:Key="mw_restore">復元</s:String>
<s:String x:Key="mw_source_code">ソースコード</s:String>
<s:String x:Key="mw_made_by">k1tbyte が ❤️ を込めて作成</s:String>
<s:String x:Key="mw_star_hint">役に立ったらスターをつけてください ;)</s:String>
<s:String x:Key="mw_copy_logs">ログをクリップボードにコピー</s:String>
<s:String x:Key="mw_export_logs">ログをファイルにエクスポート</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">設定</s:String>
<s:String x:Key="settings_language">言語</s:String>
<s:String x:Key="settings_save">保存</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">WeMod Pro を有効化</s:String>
<s:String x:Key="pv_devtools">F12でDevTools</s:String>
<s:String x:Key="pv_disable_updates">アップデートを無効化</s:String>
<s:String x:Key="pv_remote_web_panel_preview">リモートアクセスを有効化(ベータ)</s:String>
<s:String x:Key="pv_custom_scripts">カスタムスクリプト</s:String>
<s:String x:Key="pv_add_js_scripts">.js を追加</s:String>
<s:String x:Key="pv_custom_scripts_hint">選択した .js ファイルは Wand に組み込まれ、レンダラーで読み込まれます。</s:String>
<s:String x:Key="pv_no_custom_scripts">スクリプトが選択されていません</s:String>
<s:String x:Key="pv_start">開始</s:String>
<s:String x:Key="pv_popup_title">何を改善しますか?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">アップデート前に、変更が適用されている場合はロールバックすることを強くお勧めします</s:String>
<s:String x:Key="up_update_now">今すぐ更新</s:String>
<s:String x:Key="up_popup_title">アップデート利用可能!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Polski</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Dostępna jest nowa wersja</s:String>
<s:String x:Key="mw_folder_path">Ścieżka folderu</s:String>
<s:String x:Key="mw_folder_not_found">Folder nie znaleziony</s:String>
<s:String x:Key="mw_patch">Zastosuj</s:String>
<s:String x:Key="mw_restore">Przywróć</s:String>
<s:String x:Key="mw_source_code">Kod źródłowy</s:String>
<s:String x:Key="mw_made_by">Wykonane z ❤️ przez k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Daj gwiazdkę, jeśli ci pomogło ;)</s:String>
<s:String x:Key="mw_copy_logs">Skopiuj logi do schowka</s:String>
<s:String x:Key="mw_export_logs">Eksportuj logi do pliku</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Ustawienia</s:String>
<s:String x:Key="settings_language">Język</s:String>
<s:String x:Key="settings_save">Zapisz</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Aktywuj WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools na F12</s:String>
<s:String x:Key="pv_disable_updates">Wyłącz aktualizacje</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Włącz zdalny dostęp (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Skrypty niestandardowe</s:String>
<s:String x:Key="pv_add_js_scripts">Dodaj .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Wybrane pliki .js są pakowane do Wand i ładowane w rendererze.</s:String>
<s:String x:Key="pv_no_custom_scripts">Nie wybrano skryptów</s:String>
<s:String x:Key="pv_start">Rozpocznij</s:String>
<s:String x:Key="pv_popup_title">Co będziemy ulepszać?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Przed aktualizacją zdecydowanie zaleca się cofnięcie zmian, jeśli zostały zastosowane</s:String>
<s:String x:Key="up_update_now">Aktualizuj teraz</s:String>
<s:String x:Key="up_popup_title">Dostępna aktualizacja!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Português</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Uma nova versão está disponível</s:String>
<s:String x:Key="mw_folder_path">Caminho da pasta</s:String>
<s:String x:Key="mw_folder_not_found">Pasta não encontrada</s:String>
<s:String x:Key="mw_patch">Aplicar</s:String>
<s:String x:Key="mw_restore">Restaurar</s:String>
<s:String x:Key="mw_source_code">Código fonte</s:String>
<s:String x:Key="mw_made_by">Feito com ❤️ por k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Dê uma estrela se isso te ajudou ;)</s:String>
<s:String x:Key="mw_copy_logs">Copiar logs para a área de transferência</s:String>
<s:String x:Key="mw_export_logs">Exportar logs para arquivo</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Configurações</s:String>
<s:String x:Key="settings_language">Idioma</s:String>
<s:String x:Key="settings_save">Salvar</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Ativar WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools no F12</s:String>
<s:String x:Key="pv_disable_updates">Desativar atualizações</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Ativar acesso remoto (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Scripts personalizados</s:String>
<s:String x:Key="pv_add_js_scripts">Adicionar .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Os arquivos .js selecionados são empacotados no Wand e carregados no renderer.</s:String>
<s:String x:Key="pv_no_custom_scripts">Nenhum script selecionado</s:String>
<s:String x:Key="pv_start">Iniciar</s:String>
<s:String x:Key="pv_popup_title">O que vamos melhorar?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Antes de atualizar, é altamente recomendável reverter as modificações se elas foram aplicadas</s:String>
<s:String x:Key="up_update_now">Atualizar agora</s:String>
<s:String x:Key="up_popup_title">Atualização disponível!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Русский</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Доступна новая версия</s:String>
<s:String x:Key="mw_folder_path">Путь к папке</s:String>
<s:String x:Key="mw_folder_not_found">Папка не найдена</s:String>
<s:String x:Key="mw_patch">Применить</s:String>
<s:String x:Key="mw_restore">Восстановить</s:String>
<s:String x:Key="mw_source_code">Исходный код</s:String>
<s:String x:Key="mw_made_by">Сделано с ❤️ by k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Поставьте звезду, если это было полезно ;)</s:String>
<s:String x:Key="mw_copy_logs">Скопировать логи в буфер обмена</s:String>
<s:String x:Key="mw_export_logs">Экспортировать логи в файл</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Настройки</s:String>
<s:String x:Key="settings_language">Язык</s:String>
<s:String x:Key="settings_save">Сохранить</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Активировать WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools на F12</s:String>
<s:String x:Key="pv_disable_updates">Отключить обновления</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Remote-доступ (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Свои скрипты</s:String>
<s:String x:Key="pv_add_js_scripts">Добавить .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Выбранные .js попадут в Wand и загрузятся в renderer.</s:String>
<s:String x:Key="pv_no_custom_scripts">Скрипты не выбраны</s:String>
<s:String x:Key="pv_start">Начать</s:String>
<s:String x:Key="pv_popup_title">Что будем улучшать?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Перед обновлением настоятельно рекомендуется откатить изменения, если они были применены</s:String>
<s:String x:Key="up_update_now">Обновить сейчас</s:String>
<s:String x:Key="up_popup_title">Доступно обновление!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Türkçe</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Yeni bir sürüm mevcut</s:String>
<s:String x:Key="mw_folder_path">Klasör yolu</s:String>
<s:String x:Key="mw_folder_not_found">Klasör bulunamadı</s:String>
<s:String x:Key="mw_patch">Uygula</s:String>
<s:String x:Key="mw_restore">Geri Yükle</s:String>
<s:String x:Key="mw_source_code">Kaynak kodu</s:String>
<s:String x:Key="mw_made_by">k1tbyte tarafından ❤️ ile yapıldı</s:String>
<s:String x:Key="mw_star_hint">Yardımcı olduysa yıldız verin ;)</s:String>
<s:String x:Key="mw_copy_logs">Günlükleri panoya kopyala</s:String>
<s:String x:Key="mw_export_logs">Günlükleri dosyaya aktar</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Ayarlar</s:String>
<s:String x:Key="settings_language">Dil</s:String>
<s:String x:Key="settings_save">Kaydet</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">WeMod Pro'yu Etkinleştir</s:String>
<s:String x:Key="pv_devtools">F12 ile DevTools</s:String>
<s:String x:Key="pv_disable_updates">Güncellemeleri devre dışı bırak</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Uzaktan erişimi etkinleştir (beta)</s:String>
<s:String x:Key="pv_custom_scripts">Özel betikler</s:String>
<s:String x:Key="pv_add_js_scripts">.js ekle</s:String>
<s:String x:Key="pv_custom_scripts_hint">Seçilen .js dosyaları Wand içine paketlenir ve renderer'da yüklenir.</s:String>
<s:String x:Key="pv_no_custom_scripts">Betik seçilmedi</s:String>
<s:String x:Key="pv_start">Başlat</s:String>
<s:String x:Key="pv_popup_title">Neyi geliştireceğiz?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Güncellemeden önce, değişiklikler uygulandıysa geri almak şiddetle tavsiye edilir</s:String>
<s:String x:Key="up_update_now">Şimdi güncelle</s:String>
<s:String x:Key="up_popup_title">Güncelleme mevcut!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">Українська</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">Доступна нова версія</s:String>
<s:String x:Key="mw_folder_path">Шлях до папки</s:String>
<s:String x:Key="mw_folder_not_found">Папку не знайдено</s:String>
<s:String x:Key="mw_patch">Застосувати</s:String>
<s:String x:Key="mw_restore">Відновити</s:String>
<s:String x:Key="mw_source_code">Вихідний код</s:String>
<s:String x:Key="mw_made_by">Зроблено з ❤️ by k1tbyte</s:String>
<s:String x:Key="mw_star_hint">Поставте зірку, якщо це було корисно ;)</s:String>
<s:String x:Key="mw_copy_logs">Скопіювати логи до буфера обміну</s:String>
<s:String x:Key="mw_export_logs">Експортувати логи у файл</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">Налаштування</s:String>
<s:String x:Key="settings_language">Мова</s:String>
<s:String x:Key="settings_save">Зберегти</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">Активувати WeMod Pro</s:String>
<s:String x:Key="pv_devtools">DevTools на F12</s:String>
<s:String x:Key="pv_disable_updates">Вимкнути оновлення</s:String>
<s:String x:Key="pv_remote_web_panel_preview">Увімкнути віддалений доступ (бета)</s:String>
<s:String x:Key="pv_custom_scripts">Користувацькі скрипти</s:String>
<s:String x:Key="pv_add_js_scripts">Додати .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">Вибрані файли .js пакуються у Wand і завантажуються в рендерері.</s:String>
<s:String x:Key="pv_no_custom_scripts">Скрипти не вибрано</s:String>
<s:String x:Key="pv_start">Почати</s:String>
<s:String x:Key="pv_popup_title">Що будемо покращувати?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">Перед оновленням наполегливо рекомендується відкотити зміни, якщо вони були застосовані</s:String>
<s:String x:Key="up_update_now">Оновити зараз</s:String>
<s:String x:Key="up_popup_title">Доступне оновлення!</s:String>
<!--#endregion -->
</ResourceDictionary>
+48
View File
@@ -0,0 +1,48 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<!--#region Language Metadata -->
<s:String x:Key="language_display_name">简体中文</s:String>
<!--#endregion -->
<!--#region MainWindow -->
<s:String x:Key="mw_title">WandEnhancer</s:String>
<s:String x:Key="mw_update_available">有新版本可用</s:String>
<s:String x:Key="mw_folder_path">文件夹路径</s:String>
<s:String x:Key="mw_folder_not_found">未找到文件夹</s:String>
<s:String x:Key="mw_patch">增强</s:String>
<s:String x:Key="mw_restore">恢复</s:String>
<s:String x:Key="mw_source_code">源代码</s:String>
<s:String x:Key="mw_made_by">由 k1tbyte 用 ❤️ 制作</s:String>
<s:String x:Key="mw_star_hint">如果这对您有帮助,请给个星标 ;)</s:String>
<s:String x:Key="mw_copy_logs">复制日志到剪贴板</s:String>
<s:String x:Key="mw_export_logs">将日志导出到文件</s:String>
<!--#endregion -->
<!--#region Settings -->
<s:String x:Key="settings_title">设置</s:String>
<s:String x:Key="settings_language">语言</s:String>
<s:String x:Key="settings_save">保存</s:String>
<!--#endregion -->
<!--#region EnhanceVectorsPopup -->
<s:String x:Key="pv_activate_pro">激活 WeMod Pro</s:String>
<s:String x:Key="pv_devtools">按 F12 打开开发者工具</s:String>
<s:String x:Key="pv_disable_updates">禁用更新</s:String>
<s:String x:Key="pv_remote_web_panel_preview">启用远程访问(beta</s:String>
<s:String x:Key="pv_custom_scripts">自定义脚本</s:String>
<s:String x:Key="pv_add_js_scripts">添加 .js</s:String>
<s:String x:Key="pv_custom_scripts_hint">选中的 .js 文件会打包到 Wand 并在渲染器中加载。</s:String>
<s:String x:Key="pv_no_custom_scripts">未选择脚本</s:String>
<s:String x:Key="pv_start">开始</s:String>
<s:String x:Key="pv_popup_title">我们要增强什么?</s:String>
<!--#endregion -->
<!--#region UpdatePopup -->
<s:String x:Key="up_warning">在更新之前,强烈建议回滚已应用的修改</s:String>
<s:String x:Key="up_update_now">立即更新</s:String>
<s:String x:Key="up_popup_title">有更新可用!</s:String>
<!--#endregion -->
</ResourceDictionary>
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using WandEnhancer.Utils;
namespace WandEnhancer.Models
{
public enum EPatchType
{
ActivatePro = 1,
DisableUpdates = 2,
DisableTelemetry = 4,
DevToolsOnF12 = 8,
RemoteWebPanelPreview = 16
}
public sealed class PatchConfig
{
private string _path;
public HashSet<EPatchType> PatchTypes { get; set; }
public List<string> CustomScriptPaths { get; set; } = new List<string>();
public bool AutoApplyPatches { get; set; }
[JsonIgnore]
public WeModConfig AppProps { get; private set; }
public string Path
{
get => _path;
set
{
_path = value;
AppProps = Extensions.CheckWeModPath(_path) ?? throw new Exception("Invalid WeMod path");
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
using System;
namespace WandEnhancer.Models
{
public sealed class Signature
{
public readonly byte[] OriginalBytes;
public readonly byte[] PatchBytes;
public readonly byte[] Sequence;
public readonly byte[] Mask;
public readonly int Offset;
public int Length => Sequence.Length;
public static implicit operator byte[](Signature signature) => signature.Sequence;
public Signature(string signature, int offset, byte[] patchBytes, byte[] originalBytes)
{
Parse(signature, out Sequence, out Mask);
PatchBytes = patchBytes;
OriginalBytes = originalBytes;
Offset = offset;
}
private static void Parse(string signatureStr, out byte[] pattern, out byte[] mask)
{
var parts = signatureStr.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
var length = parts.Length;
pattern = new byte[length];
mask = new byte[length];
for (var i = 0; i < length; i++)
{
if (parts[i] == "??" || parts[i] == "?")
{
pattern[i] = 0;
// wildcard byte
mask[i] = 0;
continue;
}
pattern[i] = Convert.ToByte(parts[i], 16);
mask[i] = 1;
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace WandEnhancer.Models
{
public class WeModConfig
{
public string BrandName { get; set; }
public string ExecutableName { get; set; }
public string RootDirectory { get; set; }
[JsonIgnore]
public string ExecutablePath => System.IO.Path.Combine(RootDirectory, ExecutableName);
public override string ToString()
{
return RootDirectory;
}
}
}
@@ -1,16 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using WeModPatcher.Core;
using WeModPatcher.Models;
using WeModPatcher.Utils;
using WeModPatcher.View.MainWindow;
using WandEnhancer.View.MainWindow;
namespace WeModPatcher
namespace WandEnhancer
{
public static class Program
{
@@ -23,28 +17,7 @@ namespace WeModPatcher
List<LogEntry> logEntries = new List<LogEntry>();
if (args.Length > 0)
{
try
{
var patchConfig = JsonConvert.DeserializeObject<PatchConfig>(Extensions.Base64Decode(args[0]));
RuntimePatcher.Patch(patchConfig, (message, type) =>
{
logEntries.Add(new LogEntry
{
Message = message,
LogType = type
});
});
Environment.Exit(0);
}
catch (Exception e)
{
logEntries.Add(new LogEntry
{
Message = "Runtime patching failed: " + e.Message,
LogType = ELogType.Error
});
}
// TODO: Command line arguments handling
}
var application = new App();
@@ -7,11 +7,11 @@ using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("WeModPatcher")]
[assembly: AssemblyTitle("WandEnhancer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("WeModPatcher")]
[assembly: AssemblyProduct("WandEnhancer")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -51,5 +51,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.2.0")]
[assembly: AssemblyFileVersion("1.0.2.0")]
[assembly: AssemblyVersion("1.0.7.0")]
[assembly: AssemblyFileVersion("1.0.7.0")]
@@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace WeModPatcher.Properties
namespace WandEnhancer.Properties
{
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
@@ -45,7 +45,7 @@ namespace WeModPatcher.Properties
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp =
new global::System.Resources.ResourceManager("WeModPatcher.Properties.Resources",
new global::System.Resources.ResourceManager("WandEnhancer.Properties.Resources",
typeof(Resources).Assembly);
resourceMan = temp;
}
@@ -3,7 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
namespace WeModPatcher.ReactiveUICore
namespace WandEnhancer.ReactiveUICore
{
public sealed class AsyncRelayCommand : ICommand
{
@@ -1,7 +1,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WeModPatcher.ReactiveUICore
namespace WandEnhancer.ReactiveUICore
{
public class ObservableObject : INotifyPropertyChanged
{
@@ -1,7 +1,7 @@
using System;
using System.Windows.Input;
namespace WeModPatcher.ReactiveUICore
namespace WandEnhancer.ReactiveUICore
{
public sealed class RelayCommand : ICommand
{
@@ -27,6 +27,14 @@
<Geometry x:Key="ArrowLeft">
M5.05 11.94l5-5v3.99H19l-.03 2.01H10.05v4Z
</Geometry>
<Geometry x:Key="CopyIcon">
M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1Z
</Geometry>
<Geometry x:Key="ExportIcon">
M14 13h-3v3H9v-3H6v-2h3V8h2v3h3m-1-9H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V9l-7-7M5 4h7v5h5v11H5V4Z
</Geometry>
<!--<Geometry x:Key="">
@@ -211,7 +211,7 @@
</Grid.ColumnDefinitions>
<Border BorderBrush="{DynamicResource Border}"
BorderThickness="0 0 1 0" IsHitTestVisible="False">
<TextBlock Text="{TemplateBinding Uid}"
<TextBlock Text="{DynamicResource mw_folder_path}"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource MutedForeground}"
@@ -225,7 +225,7 @@
<TextBlock IsHitTestVisible="False"
Grid.Column="1"
Opacity="0.3"
Text="{TemplateBinding Tag}"
Text="{DynamicResource mw_folder_not_found}"
Margin="7 0 5 1"
VerticalAlignment="Center"
Visibility="Collapsed"
@@ -242,4 +242,113 @@
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground" Value="{DynamicResource Foreground}"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="10 0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBox}">
<Grid>
<ToggleButton x:Name="ToggleButton"
Focusable="False"
Background="Transparent"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press">
<ToggleButton.Template>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border x:Name="Border" CornerRadius="3"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="Transparent">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="1" BorderBrush="{DynamicResource Border}"
BorderThickness="1 0 0 0" Margin="0 5"/>
<Path x:Name="Arrow" Grid.Column="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M0,0 L4,4 L8,0" Stroke="{DynamicResource MutedForeground}"
StrokeThickness="1.5"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Secondary}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter x:Name="ContentSite"
IsHitTestVisible="False"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
Margin="10,0,25,0"
VerticalAlignment="Center"
HorizontalAlignment="Left"/>
<Popup x:Name="Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Grid x:Name="DropDown"
SnapsToDevicePixels="True"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border x:Name="DropDownBorder"
CornerRadius="3"
Margin="0 2 0 0"
Background="{DynamicResource Background}"
BorderBrush="{DynamicResource Border}"
BorderThickness="1">
<ScrollViewer Margin="4" SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Foreground" Value="{DynamicResource Foreground}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="8 5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ComboBoxItem}">
<Border x:Name="Border" CornerRadius="3" Padding="{TemplateBinding Padding}"
Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Secondary}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource Accent}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
+54
View File
@@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
namespace WandEnhancer.Utils
{
public static class Common
{
public static void TryKillProcess(string processName)
{
Process[] processes = Process.GetProcessesByName(processName);
for (int i = 0; processes.Length > i || i < 5; i++)
{
foreach (var process in processes)
{
try
{
process.Kill();
}
catch
{
// ignored
}
}
processes = Process.GetProcessesByName(processName);
Thread.Sleep(250);
}
if (processes.Length > 0)
{
throw new Exception("Failed to kill WeMod");
}
}
public static string GetCurrentDir()
{
var assemblyLocation = Assembly.GetExecutingAssembly().Location;
return Path.GetDirectoryName(assemblyLocation) ?? throw new InvalidOperationException();
}
public static string ComputeSha256Hash(string input)
{
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
var bytes = System.Text.Encoding.UTF8.GetBytes(input);
var hashBytes = sha256.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
}
}
@@ -2,36 +2,54 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using WandEnhancer.Models;
namespace WeModPatcher.Utils
namespace WandEnhancer.Utils
{
public static class Extensions
{
public static bool CheckWeModPath(string root)
public static WeModConfig CheckWeModPath(string versionRoot)
{
try
{
return File.Exists(Path.Combine(root, "WeMod.exe")) &&
File.Exists(Path.Combine(root, "resources", "app.asar"));
foreach (var name in Constants.WeModBrandNames)
{
var exeName = $"{name}.exe";
var path = Path.Combine(versionRoot, exeName);
if (File.Exists(path) && File.Exists(Path.Combine(versionRoot, "resources", "app.asar")))
{
return new WeModConfig
{
BrandName = name,
ExecutableName = exeName,
RootDirectory = versionRoot
};
}
}
}
catch
{
return false;
// ignored
}
return null;
}
public static string FindWeModDirectory()
public static WeModConfig FindWeMod()
{
string localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
string defaultDir = Path.Combine(localAppDataPath ?? "", "WeMod");
if (!Directory.Exists(defaultDir))
foreach (var folder in Constants.WeModBrandNames)
{
return null;
var weModDir = Path.Combine(localAppDataPath ?? "", folder);
if(Directory.Exists(weModDir))
{
return FindLatestWeMod(weModDir);
}
}
return FindLatestWeMod(defaultDir);
return null;
}
public static string Base64Decode(string base64EncodedData)
@@ -46,7 +64,7 @@ namespace WeModPatcher.Utils
return System.Convert.ToBase64String(plainTextBytes);
}
public static string FindLatestWeMod(string root)
public static WeModConfig FindLatestWeMod(string root)
{
var appFolders = Directory.EnumerateDirectories(root)
.Select(folderPath => new DirectoryInfo(folderPath))
@@ -59,13 +77,11 @@ namespace WeModPatcher.Utils
})
.OrderByDescending(item => item.LastModified)
.ToList();
return (
from folder
in appFolders
where CheckWeModPath(folder.Path)
select folder.Path
).FirstOrDefault();
return appFolders
.Select(folder => CheckWeModPath(folder.Path))
.FirstOrDefault(config => config != null);
}
}
}
@@ -8,7 +8,7 @@ using System.Net.Http;
using System.Windows;
using Newtonsoft.Json;
namespace WeModPatcher.Utils
namespace WandEnhancer.Utils
{
public class GitHubRelease
{
@@ -1,7 +1,7 @@
using System;
using System.Runtime.InteropServices;
namespace WeModPatcher.Utils.Win32
namespace WandEnhancer.Utils.Win32
{
public class Shortcut
{
@@ -1,9 +1,9 @@
<UserControl x:Class="WeModPatcher.View.Controls.InfoItem"
<UserControl x:Class="WandEnhancer.View.Controls.InfoItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WeModPatcher.View.Controls"
xmlns:local="clr-namespace:WandEnhancer.View.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
@@ -2,7 +2,7 @@
using System.Windows.Controls;
using System.Windows.Media;
namespace WeModPatcher.View.Controls
namespace WandEnhancer.View.Controls
{
public partial class InfoItem : UserControl
{
@@ -1,9 +1,9 @@
<Grid x:Class="WeModPatcher.View.Controls.PopupHost"
<Grid x:Class="WandEnhancer.View.Controls.PopupHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WeModPatcher.View.Controls"
xmlns:local="clr-namespace:WandEnhancer.View.Controls"
mc:Ignorable="d"
Visibility="Collapsed">
<Border x:Name="Splash" Background="Black" CornerRadius="7"
@@ -5,7 +5,7 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;
namespace WeModPatcher.View.Controls
namespace WandEnhancer.View.Controls
{
public partial class PopupHost : Grid
{
@@ -1,4 +1,4 @@
namespace WeModPatcher.View.MainWindow
namespace WandEnhancer.View.MainWindow
{
public enum ELogType
{
@@ -1,13 +1,13 @@
<Window x:Class="WeModPatcher.View.MainWindow.MainWindow"
<Window x:Class="WandEnhancer.View.MainWindow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WeModPatcher.View.MainWindow"
xmlns:controls="clr-namespace:WeModPatcher.View.Controls"
xmlns:local="clr-namespace:WandEnhancer.View.MainWindow"
xmlns:controls="clr-namespace:WandEnhancer.View.Controls"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:MainWindowVm}"
Title="WeMod Patcher"
Title="WandEnhancer"
Height="510" MaxHeight="510"
Width="780" MaxWidth="780"
Opacity="0.97"
@@ -36,7 +36,7 @@
VerticalAlignment="Center"
FontSize="18" Margin="10 0 0 0">
<Bold>
WeMod Patcher
WandEnhancer
</Bold>
</TextBlock>
<TextBlock x:Name="VersionLabel" VerticalAlignment="Bottom"
@@ -50,10 +50,17 @@
ToolTip="Click to update"
Command="{Binding UpdateCommand}"
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource ToVisibilityConverter}}"
Content="A new version is available"/>
Content="{DynamicResource mw_update_available}"/>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
Margin="0 0 5 0"
Width="25" Height="25" Padding="5.5"
Style="{StaticResource IconButton}"
Tag="{StaticResource CogIcon}"
Command="{Binding OpenSettingsCommand}"
/>
<Button
Margin="9 0 15 0"
Tag="{StaticResource CloseIcon}"
@@ -89,9 +96,9 @@
<Grid Margin="10" Cursor="Hand" Background="Transparent">
<TextBox Style="{StaticResource TitledTextBox}"
Uid="Folder path" IsReadOnly="True"
Text="{Binding WeModPath}"
VerticalAlignment="Center" Tag="Folder not found">
IsReadOnly="True"
Text="{Binding WeModInfo.RootDirectory, Mode=OneWay}"
VerticalAlignment="Center">
</TextBox>
<Grid.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding SetFolderPathCommand}" />
@@ -102,7 +109,8 @@
<Border Grid.Row="1" BorderBrush="{DynamicResource Border}" BorderThickness="1"
Margin="10 0 10 10"
CornerRadius="5">
<ListBox ItemsSource="{Binding LogList}" SelectionMode="Single"
<Grid>
<ListBox ItemsSource="{Binding LogList}" SelectionMode="Single"
BorderBrush="Transparent" BorderThickness="0"
Background="Transparent"
x:Name="LogList"
@@ -145,6 +153,20 @@
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0 4 4 0">
<Button Width="22" Height="22" Padding="4"
Style="{StaticResource IconButton}"
Tag="{StaticResource CopyIcon}"
ToolTip="{DynamicResource mw_copy_logs}"
Command="{Binding CopyLogsCommand}"/>
<Button Width="22" Height="22" Padding="4" Margin="4 0 0 0"
Style="{StaticResource IconButton}"
Tag="{StaticResource ExportIcon}"
ToolTip="{DynamicResource mw_export_logs}"
Command="{Binding ExportLogsCommand}"/>
</StackPanel>
</Grid>
</Border>
@@ -163,14 +185,15 @@
<Button Style="{StaticResource ColoredButton}"
IsEnabled="{Binding IsPatchEnabled}"
FontWeight="Bold" FontSize="16" Width="200"
Command="{Binding ApplyPatchCommand}">Patch</Button>
Command="{Binding ApplyPatchCommand}"
Content="{DynamicResource mw_patch}"/>
</Grid>
<Button HorizontalAlignment="Right"
Command="{Binding RestoreBackupCommand }"
FontWeight="Bold" FontSize="16" Width="200"
Style="{StaticResource ColoredButton}"
Visibility="{Binding AlreadyPatched, Converter={StaticResource ToVisibilityConverter}}"
Content="Restore"/>
Content="{DynamicResource mw_restore}"/>
</Grid>
</StackPanel>
@@ -188,13 +211,13 @@
</Viewbox>
<Grid>
<TextBlock Margin="8 0 0 0" FontSize="10" Foreground="{DynamicResource AccentForeground}">
<Hyperlink Foreground="{DynamicResource AccentForeground}">Source code </Hyperlink>
<Hyperlink Foreground="{DynamicResource AccentForeground}"><Run Text="{DynamicResource mw_source_code}"/></Hyperlink>
<LineBreak/>
<Run>Made with ❤️ by k1tbyte</Run>
<Run Text="{DynamicResource mw_made_by}"/>
<LineBreak/>
<Run Foreground="{DynamicResource MutedForeground}">Put a star if you found this helpful ;)</Run>
<Run Foreground="{DynamicResource MutedForeground}" Text="{DynamicResource mw_star_hint}"/>
</TextBlock>
</Grid>
</StackPanel>
@@ -3,7 +3,7 @@ using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace WeModPatcher.View.MainWindow
namespace WandEnhancer.View.MainWindow
{
/// <summary>
/// Interaction logic for MainWindow.xaml
@@ -1,47 +1,45 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Threading;
using AsarSharp;
using WeModPatcher.Core;
using WeModPatcher.Models;
using WeModPatcher.ReactiveUICore;
using WeModPatcher.Utils;
using WeModPatcher.View.Popups;
using WandEnhancer.Core;
using WandEnhancer.Models;
using WandEnhancer.ReactiveUICore;
using WandEnhancer.Utils;
using WandEnhancer.View.Popups;
using Application = System.Windows.Application;
namespace WeModPatcher.View.MainWindow
namespace WandEnhancer.View.MainWindow
{
public class MainWindowVm : ObservableObject
{
private readonly MainWindow _view;
public ObservableCollection<LogEntry> LogList { get; set; } = new ObservableCollection<LogEntry>();
private static Updater _updater = new Updater();
private string _weModPath;
public string WeModPath
private WeModConfig _weModConfig;
public WeModConfig WeModInfo
{
get => _weModPath;
get => _weModConfig;
set
{
SetProperty(ref _weModPath, value);
SetProperty(ref _weModConfig, value);
if (value == null) return;
Log($"WeMod directory found at '{_weModPath}'", ELogType.Success);
if (File.Exists(Path.Combine(_weModPath, "resources", "app.asar.backup")))
Log($"WeMod directory found at '{_weModConfig}' ({_weModConfig.ExecutableName})", ELogType.Success);
if (File.Exists(Path.Combine(_weModConfig.RootDirectory, "resources", "app.asar.backup")))
{
Log("WeMod already patched. If you want to patch again, please restore the backup first.", ELogType.Warn);
Log("WeMod already patched. If you want to patch again, please restore the backup first.",
ELogType.Warn);
IsPatchEnabled = false;
AlreadyPatched = true;
return;
}
Log("Ready for patching.", ELogType.Info);
IsPatchEnabled = true;
}
@@ -54,26 +52,31 @@ namespace WeModPatcher.View.MainWindow
get => _isPatchEnabled;
set => SetProperty(ref _isPatchEnabled, value);
}
private bool _alreadyPatched;
public bool AlreadyPatched
{
get => _alreadyPatched;
set => SetProperty(ref _alreadyPatched, value);
}
private bool _isUpdateAvailable;
public bool IsUpdateAvailable
{
get => _isUpdateAvailable;
set => SetProperty(ref _isUpdateAvailable, value);
}
public RelayCommand SetFolderPathCommand { get; }
public RelayCommand ApplyPatchCommand { get; }
public RelayCommand RestoreBackupCommand { get; }
public AsyncRelayCommand UpdateCommand { get; }
public RelayCommand UpdateCommand { get; }
public RelayCommand OpenSettingsCommand { get; }
public RelayCommand CopyLogsCommand { get; }
public RelayCommand ExportLogsCommand { get; }
private void OnFolderPathSelection(object obj)
{
using (var dialog = new FolderBrowserDialog())
@@ -86,9 +89,11 @@ namespace WeModPatcher.View.MainWindow
string selectedPath = dialog.SelectedPath;
string fileName = Path.GetFileName(selectedPath);
if (Extensions.CheckWeModPath(selectedPath))
var info = Extensions.CheckWeModPath(selectedPath);
if (info != null)
{
WeModPath = selectedPath;
WeModInfo = info;
return;
}
@@ -102,34 +107,25 @@ namespace WeModPatcher.View.MainWindow
private void OnBackupRestoring(object param)
{
var backupPath = Path.Combine(WeModPath, "resources", "app.asar.backup");
var backupPath = Path.Combine(WeModInfo.RootDirectory, "resources", "app.asar.backup");
if (!File.Exists(backupPath))
{
Log("Backup not found. Please dont delete it manually", ELogType.Error);
return;
}
try
{
// Try to lock the file to see if it's in use
using (File.Open(backupPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
}
// This shit doesn't look at the hash and verify() always returns true
//using X509Certificate2 cert = new X509Certificate2(X509Certificate.CreateFromSignedFile(filePath));
var restoreExeResult = MemoryUtils.PatchFile( Path.Combine( WeModPath, "WeMod.exe"),
Constants.ExePatchSignature, Constants.ExePatchSignature.OriginalBytes);
if (restoreExeResult == -1)
var proxyDllPath = Path.Combine(WeModInfo.RootDirectory, "version.dll");
if(File.Exists(proxyDllPath))
{
Log("Failed to restore the backup. Please close the WeMod and try again.", ELogType.Error);
}
else
{
Log(restoreExeResult == 0 ?
"Signature exe is original, does not require restoration"
: "WeMod.exe restored successfully", ELogType.Success);
File.Delete(proxyDllPath);
}
}
catch
@@ -137,8 +133,8 @@ namespace WeModPatcher.View.MainWindow
Log("Backup file is locked. Please close the WeMod and try again.", ELogType.Error);
return;
}
File.Copy(backupPath, Path.Combine(WeModPath, "resources", "app.asar"), true);
File.Copy(backupPath, Path.Combine(WeModInfo.RootDirectory, "resources", "app.asar"), true);
File.Delete(backupPath);
Log("Backup restored successfully.", ELogType.Success);
AlreadyPatched = false;
@@ -147,13 +143,13 @@ namespace WeModPatcher.View.MainWindow
private void OnPatching(object param)
{
if (WeModPath == null)
if (WeModInfo == null)
{
Log("Can't be done. Please specify the directory first.", ELogType.Warn);
return;
}
MainWindow.Instance.OpenPopup(new PatchVectorsPopup( async config =>
MainWindow.Instance.OpenPopup(new PatchVectorsPopup(async config =>
{
MainWindow.Instance.ClosePopup();
IsPatchEnabled = false;
@@ -161,7 +157,7 @@ namespace WeModPatcher.View.MainWindow
{
try
{
new StaticPatcher(WeModPath, Log, config).Patch();
new Enhancer(WeModInfo, Log, config).Patch();
AlreadyPatched = true;
}
catch (Exception e)
@@ -170,8 +166,7 @@ namespace WeModPatcher.View.MainWindow
IsPatchEnabled = true;
}
});
}), "What are we gonna patch?");
}), Application.Current.FindResource("pv_popup_title") as string);
}
private void Log(string message, ELogType logType)
@@ -190,24 +185,90 @@ namespace WeModPatcher.View.MainWindow
});
}
private async Task OnUpdate(object param)
private void OnUpdate(object param)
{
await Task.Run(async () =>
MainWindow.Instance.OpenPopup(new UpdatePopup(() =>
{
Task.Run(async () =>
{
try
{
await _updater.Update();
}
catch (Exception e)
{
Log($"Failed to update: {e.Message}", ELogType.Error);
return;
}
Log("WandEnhancer updated successfully. Restarting...", ELogType.Success);
});
}), Application.Current.FindResource("up_popup_title") as string);
}
private void OnOpenSettings(object param)
{
MainWindow.Instance.OpenPopup(new SettingsPopup(), Application.Current.FindResource("settings_title") as string);
}
private string BuildLogReport()
{
var builder = new StringBuilder();
foreach (var entry in LogList)
{
builder.AppendLine(entry.Message);
}
return builder.ToString();
}
private void OnCopyLogs(object param)
{
if (LogList.Count == 0)
{
return;
}
try
{
System.Windows.Clipboard.SetText(BuildLogReport());
Log("Logs copied to clipboard.", ELogType.Success);
}
catch (Exception e)
{
Log($"Failed to copy logs: {e.Message}", ELogType.Error);
}
}
private void OnExportLogs(object param)
{
if (LogList.Count == 0)
{
return;
}
using (var dialog = new SaveFileDialog
{
Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*",
FileName = $"wand-enhancer-log-{DateTime.Now:yyyyMMdd-HHmmss}.txt"
})
{
if (dialog.ShowDialog() != DialogResult.OK)
{
return;
}
try
{
await _updater.Update();
File.WriteAllText(dialog.FileName, BuildLogReport());
Log($"Logs exported to '{dialog.FileName}'.", ELogType.Success);
}
catch (Exception e)
{
Log($"Failed to update: {e.Message}", ELogType.Error);
return;
Log($"Failed to export logs: {e.Message}", ELogType.Error);
}
Log("WeModPatcher updated successfully. Restarting...", ELogType.Success);
});
}
}
public MainWindowVm(MainWindow view)
{
Task.Run(async () => IsUpdateAvailable = await _updater.CheckForUpdates());
@@ -215,10 +276,13 @@ namespace WeModPatcher.View.MainWindow
SetFolderPathCommand = new RelayCommand(OnFolderPathSelection);
ApplyPatchCommand = new RelayCommand(OnPatching);
RestoreBackupCommand = new RelayCommand(OnBackupRestoring);
UpdateCommand = new AsyncRelayCommand(OnUpdate);
WeModPath = Extensions.FindWeModDirectory();
if (WeModPath == null)
UpdateCommand = new RelayCommand(OnUpdate);
OpenSettingsCommand = new RelayCommand(OnOpenSettings);
CopyLogsCommand = new RelayCommand(OnCopyLogs);
ExportLogsCommand = new RelayCommand(OnExportLogs);
WeModInfo = Extensions.FindWeMod();
if (WeModInfo == null)
{
Log("WeMod directory not found.", ELogType.Error);
}
@@ -0,0 +1,99 @@
<UserControl x:Class="WandEnhancer.View.Popups.PatchVectorsPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WandEnhancer.View.Popups"
xmlns:controls="clr-namespace:WandEnhancer.View.Controls"
mc:Ignorable="d"
d:DesignHeight="Auto" d:DesignWidth="Auto"
Background="{DynamicResource Background}"
Foreground="{DynamicResource MutedForeground}"
FontWeight="Medium"
FontSize="13">
<Grid MinWidth="430">
<Grid Visibility="Visible" Margin="0 0 5 0">
<Grid.RowDefinitions>
<RowDefinition Height="27" />
<RowDefinition Height="27" />
<RowDefinition Height="27" />
<RowDefinition Height="27" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Text="{DynamicResource pv_activate_pro}" />
<CheckBox Grid.Row="0" Grid.Column="1" x:Name="ActivateProBox" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="True" />
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Text="{DynamicResource pv_devtools}" />
<CheckBox Grid.Row="1" Grid.Column="1" x:Name="DevToolsHotkeyBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Text="{DynamicResource pv_disable_updates}" />
<CheckBox Grid.Row="2" Grid.Column="1" x:Name="DisableUpdateBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Text="{DynamicResource pv_remote_web_panel_preview}" />
<CheckBox Grid.Row="3" Grid.Column="1" x:Name="RemoteWebPanelPreviewBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
<Border Grid.Row="4" Grid.ColumnSpan="2" Margin="0 14 0 0" Padding="10"
BorderBrush="{DynamicResource Border}" BorderThickness="1" CornerRadius="4"
Background="{DynamicResource Muted}">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center"
Foreground="{DynamicResource Foreground}"
Text="{DynamicResource pv_custom_scripts}" />
<Button Grid.Column="1" Padding="10 4" Content="{DynamicResource pv_add_js_scripts}"
Click="OnAddScriptClick" />
</Grid>
<TextBlock Margin="0 7 0 0" FontSize="11" TextWrapping="Wrap"
Opacity="0.8"
Text="{DynamicResource pv_custom_scripts_hint}" />
<ItemsControl x:Name="ScriptList" Margin="0 2 0 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0 6 6 0" Padding="8 3"
Background="{DynamicResource Card}"
BorderBrush="{DynamicResource Border}"
BorderThickness="1"
CornerRadius="3">
<DockPanel LastChildFill="True">
<Button DockPanel.Dock="Right" Tag="{Binding}" Content="x"
BorderThickness="0" Padding="6 0" Margin="6 0 0 0"
Click="OnRemoveScriptClick" />
<TextBlock VerticalAlignment="Center"
Foreground="{DynamicResource Foreground}"
Text="{Binding FileName}" />
</DockPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock x:Name="NoScriptsText" Margin="0 7 0 0" FontSize="11"
Opacity="0.7"
Text="{DynamicResource pv_no_custom_scripts}" />
</StackPanel>
</Border>
<Button Grid.Row="5" Grid.ColumnSpan="2" Padding="0 5 0 5" Margin="0 15 0 0" Content="{DynamicResource pv_start}"
Click="OnPatchButtonClick" />
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,140 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;
using WandEnhancer.Models;
namespace WandEnhancer.View.Popups
{
public partial class PatchVectorsPopup : UserControl
{
private const string JavaScriptDialogFilter = "JavaScript files (*.js)|*.js";
private const string JavaScriptFileExtension = ".js";
private readonly Action<PatchConfig> _onApply;
private readonly ObservableCollection<SelectedScript> _selectedScripts = new ObservableCollection<SelectedScript>();
public PatchVectorsPopup(Action<PatchConfig> onApply)
{
_onApply = onApply;
InitializeComponent();
ScriptList.ItemsSource = _selectedScripts;
UpdateScriptsEmptyState();
}
private void OnAddScriptClick(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Filter = JavaScriptDialogFilter,
Multiselect = true,
CheckFileExists = true
};
if (dialog.ShowDialog() != true)
{
return;
}
foreach (var path in dialog.FileNames.Where(IsJavaScriptFile))
{
AddScript(path);
}
if (_selectedScripts.Count > 0)
{
RemoteWebPanelPreviewBox.IsChecked = true;
}
UpdateScriptsEmptyState();
}
private void OnRemoveScriptClick(object sender, RoutedEventArgs e)
{
var button = sender as Button;
var script = button?.Tag as SelectedScript;
if (script == null)
{
return;
}
_selectedScripts.Remove(script);
UpdateScriptsEmptyState();
}
private void OnPatchButtonClick(object sender, RoutedEventArgs e)
{
if (ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true &&
DevToolsHotkeyBox.IsChecked != true && RemoteWebPanelPreviewBox.IsChecked != true)
{
return;
}
var result = new HashSet<EPatchType>();
if (ActivateProBox.IsChecked == true)
{
result.Add(EPatchType.ActivatePro);
}
if (DisableUpdateBox.IsChecked == true)
{
result.Add(EPatchType.DisableUpdates);
}
if (DevToolsHotkeyBox.IsChecked == true)
{
result.Add(EPatchType.DevToolsOnF12);
}
if (RemoteWebPanelPreviewBox.IsChecked == true)
{
result.Add(EPatchType.RemoteWebPanelPreview);
}
_onApply(new PatchConfig
{
PatchTypes = result,
CustomScriptPaths = _selectedScripts.Select(script => script.FullPath).ToList(),
AutoApplyPatches = false
});
}
private void AddScript(string path)
{
var fullPath = Path.GetFullPath(path);
if (_selectedScripts.Any(script => string.Equals(script.FullPath, fullPath, StringComparison.OrdinalIgnoreCase)))
{
return;
}
_selectedScripts.Add(new SelectedScript(fullPath));
}
private static bool IsJavaScriptFile(string path)
{
return File.Exists(path) && string.Equals(Path.GetExtension(path), JavaScriptFileExtension, StringComparison.OrdinalIgnoreCase);
}
private void UpdateScriptsEmptyState()
{
NoScriptsText.Visibility = _selectedScripts.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private sealed class SelectedScript
{
public SelectedScript(string fullPath)
{
FullPath = fullPath;
FileName = Path.GetFileName(fullPath);
}
public string FullPath { get; }
public string FileName { get; }
}
}
}
@@ -0,0 +1,41 @@
<UserControl x:Class="WandEnhancer.View.Popups.SettingsPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="Auto" d:DesignWidth="Auto"
Background="{DynamicResource Background}"
Foreground="{DynamicResource MutedForeground}"
FontWeight="Medium"
FontSize="13">
<Grid MinWidth="250">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0 0 0 15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center"
Text="{DynamicResource settings_language}" />
<ComboBox Grid.Column="1" x:Name="LanguageComboBox"
Width="130"
SelectionChanged="OnLanguageSelectionChanged">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<Button Grid.Row="1" Padding="0 5 0 5" Margin="0 5 0 0"
Content="{DynamicResource settings_save}"
Click="OnSaveClick" />
</Grid>
</UserControl>
@@ -0,0 +1,68 @@
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using WandEnhancer.Core;
using WandEnhancer.Core.Services;
using WandEnhancer.View.MainWindow;
namespace WandEnhancer.View.Popups
{
public partial class SettingsPopup : UserControl
{
private CultureInfo _selectedLanguage;
public SettingsPopup()
{
InitializeComponent();
LoadLanguages();
}
private void LoadLanguages()
{
var items = LocalizationManager.SupportedLanguages
.Select(c => new LanguageItem
{
Culture = c,
DisplayName = LocalizationManager.GetLanguageDisplayName(c)
})
.ToList();
LanguageComboBox.ItemsSource = items;
var currentItem = items.FirstOrDefault(i => i.Culture.Name == LocalizationManager.CurrentLanguage?.Name);
if (currentItem != null)
{
LanguageComboBox.SelectedItem = currentItem;
}
_selectedLanguage = LocalizationManager.CurrentLanguage;
}
private void OnLanguageSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (LanguageComboBox.SelectedItem is LanguageItem item)
{
_selectedLanguage = item.Culture;
}
}
private void OnSaveClick(object sender, RoutedEventArgs e)
{
if (_selectedLanguage != null &&
(LocalizationManager.CurrentLanguage == null ||
_selectedLanguage.Name != LocalizationManager.CurrentLanguage.Name))
{
LocalizationManager.CurrentLanguage = _selectedLanguage;
}
MainWindow.MainWindow.Instance.ClosePopup();
}
private class LanguageItem
{
public CultureInfo Culture { get; set; }
public string DisplayName { get; set; }
}
}
}
+20
View File
@@ -0,0 +1,20 @@
<UserControl x:Class="WandEnhancer.View.Popups.UpdatePopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WandEnhancer.View.Popups"
mc:Ignorable="d"
d:DesignHeight="Auto" d:DesignWidth="Auto"
Background="{DynamicResource Background}"
Foreground="{DynamicResource MutedForeground}"
FontWeight="Medium"
FontSize="13">
<StackPanel>
<TextBlock Foreground="Red" MaxWidth="320" TextAlignment="Center" Text="{DynamicResource up_warning}" TextWrapping="Wrap" />
<Button Padding="0 5 0 5" Margin="0 15 0 0" Content="{DynamicResource up_update_now}"
Click="OnUpdateClick" />
</StackPanel>
</UserControl>
@@ -0,0 +1,22 @@
using System;
using System.Windows;
using System.Windows.Controls;
namespace WandEnhancer.View.Popups
{
public partial class UpdatePopup : UserControl
{
private readonly Action _onUpdate;
public UpdatePopup(Action onUpdate)
{
_onUpdate = onUpdate;
InitializeComponent();
}
private void OnUpdateClick(object sender, RoutedEventArgs e)
{
_onUpdate();
}
}
}
@@ -7,8 +7,8 @@
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>WeModPatcher</RootNamespace>
<AssemblyName>WeModPatcher</AssemblyName>
<RootNamespace>WandEnhancer</RootNamespace>
<AssemblyName>WandEnhancer</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
@@ -38,7 +38,10 @@
<Prefer32bit>false</Prefer32bit>
</PropertyGroup>
<PropertyGroup>
<StartupObject>WeModPatcher.Program</StartupObject>
<StartupObject>WandEnhancer.Program</StartupObject>
<CMakeSourceDir>..\tools\asar-fuses-bypass</CMakeSourceDir>
<CMakeBuildDir>$(CMakeSourceDir)\cmake-build-release</CMakeBuildDir>
<ProxyDllPath>$(CMakeBuildDir)\version.dll</ProxyDllPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
@@ -65,18 +68,20 @@
<Compile Include="Constants.cs" />
<Compile Include="Converters\BaseBooleanConverter.cs" />
<Compile Include="Converters\ToVisibilityConverter.cs" />
<Compile Include="Core\RuntimePatcher.cs" />
<Compile Include="Core\StaticPatcher.cs" />
<Compile Include="Core\Enhancer.cs" />
<Compile Include="Core\EnhancerConfig.cs" />
<Compile Include="Core\Services\LocalizationManager.cs" />
<Compile Include="Core\Services\SettingsManager.cs" />
<Compile Include="Models\WeModConfig.cs" />
<Compile Include="Models\PatchConfig.cs" />
<Compile Include="Models\Signature.cs" />
<Compile Include="Program.cs" />
<Compile Include="ReactiveUICore\AsyncRelayCommand.cs" />
<Compile Include="ReactiveUICore\ObservableObject.cs" />
<Compile Include="ReactiveUICore\RelayCommand.cs" />
<Compile Include="Utils\Common.cs" />
<Compile Include="Utils\Extensions.cs" />
<Compile Include="Utils\MemoryUtils.cs" />
<Compile Include="Utils\Updater.cs" />
<Compile Include="Utils\Win32\Imports.cs" />
<Compile Include="Utils\Win32\Shortcut.cs" />
<Compile Include="View\Controls\InfoItem.xaml.cs">
<DependentUpon>InfoItem.xaml</DependentUpon>
@@ -88,10 +93,28 @@
<Compile Include="View\Popups\PatchVectorsPopup.xaml.cs">
<DependentUpon>PatchVectorsPopup.xaml</DependentUpon>
</Compile>
<Compile Include="View\Popups\SettingsPopup.xaml.cs">
<DependentUpon>SettingsPopup.xaml</DependentUpon>
</Compile>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="View\Popups\UpdatePopup.xaml.cs">
<DependentUpon>UpdatePopup.xaml</DependentUpon>
</Compile>
<Page Include="Locale\lang.en-US.xaml" />
<Page Include="Locale\lang.zh-CN.xaml" />
<Page Include="Locale\lang.de-DE.xaml" />
<Page Include="Locale\lang.fr-FR.xaml" />
<Page Include="Locale\lang.es-ES.xaml" />
<Page Include="Locale\lang.it-IT.xaml" />
<Page Include="Locale\lang.pt-BR.xaml" />
<Page Include="Locale\lang.pl-PL.xaml" />
<Page Include="Locale\lang.ru-RU.xaml" />
<Page Include="Locale\lang.uk-UA.xaml" />
<Page Include="Locale\lang.ja-JP.xaml" />
<Page Include="Locale\lang.tr-TR.xaml" />
<Page Include="Style\ColorScheme.xaml" />
<Page Include="Style\Icons.xaml" />
<Page Include="Style\Styles.xaml" />
@@ -99,6 +122,8 @@
<Page Include="View\Controls\PopupHost.xaml" />
<Page Include="View\MainWindow\MainWindow.xaml" />
<Page Include="View\Popups\PatchVectorsPopup.xaml" />
<Page Include="View\Popups\SettingsPopup.xaml" />
<Page Include="View\Popups\UpdatePopup.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
@@ -129,7 +154,24 @@
<Project>{beaa604a-402a-4387-8903-a53fc913a26e}</Project>
<Name>AsarSharp</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(ProxyDllPath)">
<LogicalName>proxydll</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\web-panel\dist\**\*.*" Condition="Exists('..\web-panel\dist\index.html')">
<LogicalName>remote-panel/dist/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="..\web-panel\bridge\wand-remote-bridge.cjs" Condition="Exists('..\web-panel\bridge\wand-remote-bridge.cjs')">
<LogicalName>remote-panel/bridge.cjs</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="..\web-panel\scripts\default\*.js" Condition="Exists('..\web-panel\scripts\default')">
<LogicalName>remote-panel/renderer-scripts/%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -138,6 +180,14 @@
<Error Condition="!Exists('..\packages\ILRepack.2.0.41\build\ILRepack.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\ILRepack.2.0.41\build\ILRepack.props'))" />
</Target>
<Target Name="EmbedProxyDll" BeforeTargets="BeforeBuild">
<Error Text="Proxy DLL not found: $(ProxyDllPath)"
Condition="!Exists('$(ProxyDllPath)')" />
<Message Text="Embedding Proxy DLL as resource from $(ProxyDllPath)"
Importance="high" />
</Target>
<Target Name="ILRepack" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<PropertyGroup>
<ILRepackExe>..\packages\ILRepack.2.0.41\tools\ILRepack.exe</ILRepackExe>
-173
View File
@@ -1,173 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using WeModPatcher.Models;
using WeModPatcher.Utils;
using WeModPatcher.Utils.Win32;
using WeModPatcher.View.MainWindow;
namespace WeModPatcher.Core
{
public class RuntimePatcher
{
private readonly string _exePath;
public RuntimePatcher(string exePath)
{
_exePath = exePath;
}
public void StartProcess()
{
if(string.IsNullOrEmpty(_exePath))
{
throw new Exception("Path is not specified");
}
KillWeMod();
var startupInfo = new Imports.StartupInfo { cb = Marshal.SizeOf(typeof(Imports.StartupInfo)) };
if(!Imports.CreateProcessA(_exePath,
null,
IntPtr.Zero,
IntPtr.Zero,
false, Imports.DEBUG_PROCESS, IntPtr.Zero,
null, ref startupInfo, out var processInfo))
{
throw new Exception("Failed to create process, error code: " + Marshal.GetLastWin32Error());
}
var debugEvent = new Imports.DEBUG_EVENT();
var processIds = new Dictionary<uint, bool>();
while (Imports.WaitForDebugEvent(ref debugEvent, uint.MaxValue))
{
uint continueStatus = Imports.DBG_CONTINUE;
var code = debugEvent.dwDebugEventCode;
// Console.WriteLine("Debug event code: " + code);
if (code == Imports.CREATE_PROCESS_DEBUG_EVENT)
{
// Console.WriteLine("Spawning process: " + debugEvent.dwProcessId);
processIds.Add(debugEvent.dwProcessId, false);
}
else if (code == Imports.EXIT_PROCESS_DEBUG_EVENT)
{
processIds.Remove(debugEvent.dwProcessId);
if(processIds.Count == 0)
{
break;
}
}
else if (code == Imports.EXCEPTION_DEBUG_EVENT)
{
// pass the exception to the process
continueStatus = Imports.DBG_EXCEPTION_NOT_HANDLED;
var exceptionInfo = Imports.MapUnmanagedStructure<Imports.EXCEPTION_DEBUG_INFO>(debugEvent.Union);
// Console.WriteLine("Exception code: " + exceptionInfo.ExceptionRecord.ExceptionCode);
if (exceptionInfo.ExceptionRecord.ExceptionCode == Imports.EXCEPTION_BREAKPOINT &&
processIds.TryGetValue(debugEvent.dwProcessId, out var wasPatched) && !wasPatched)
{
var process = Process.GetProcessById((int)debugEvent.dwProcessId);
// Console.WriteLine("Scanning process: " + process.ProcessName + " " + process.Id);
var address = MemoryUtils.ScanVirtualMemory(
process.Handle,
process.Modules[0].BaseAddress,
process.Modules[0].ModuleMemorySize,
Constants.ExePatchSignature.Sequence, Constants.ExePatchSignature.Mask
);
if (address != IntPtr.Zero)
{
processIds[debugEvent.dwProcessId] = MemoryUtils.SafeWriteVirtualMemory(
process.Handle,
address + Constants.ExePatchSignature.Offset,
Constants.ExePatchSignature.PatchBytes
);
/*byte[] patchedBytes = new byte[32];
if (Imports.ReadProcessMemory(process.Handle, address, patchedBytes, patchedBytes.Length, out int bytesRead))
{
Console.WriteLine("Bytes after patching: ");
for (int i = 0; i < bytesRead; i++)
{
Console.Write($"{patchedBytes[i]:X2} ");
}
Console.WriteLine();
}*/
}
}
}
Imports.ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueStatus);
}
foreach (var entry in processIds)
{
Imports.DebugActiveProcessStop(entry.Key);
}
Imports.CloseHandle(processInfo.hProcess);
}
public static void Patch(PatchConfig config, Action<string, ELogType> logger)
{
if (config.Path == null)
{
throw new Exception("Path is not specified");
}
var parent = Directory.GetParent(config.Path)?.FullName ?? config.Path;
var latestPath = Extensions.FindLatestWeMod(parent) ?? config.Path;
if (!Extensions.CheckWeModPath(latestPath))
{
throw new Exception("Invalid WeMod path");
}
if(!File.Exists(Path.Combine(latestPath, "resources", "app.asar.backup")))
{
config.PatchMethod = EPatchProcessMethod.None;
new StaticPatcher(latestPath, logger, config).Patch();
}
new RuntimePatcher(Path.Combine(latestPath, "WeMod.exe"))
.StartProcess();
}
public static void KillWeMod()
{
Process[] processes = Process.GetProcessesByName("WeMod");
for (int i = 0; processes.Length > i || i < 5; i++)
{
foreach (var process in processes)
{
try
{
process.Kill();
}
catch
{
// ignored
}
}
processes = Process.GetProcessesByName("WeMod");
Thread.Sleep(250);
}
if (processes.Length > 0)
{
throw new Exception("Failed to kill WeMod");
}
}
}
}
-241
View File
@@ -1,241 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using AsarSharp;
using Newtonsoft.Json;
using WeModPatcher.Models;
using WeModPatcher.Utils;
using WeModPatcher.View.MainWindow;
using Application = System.Windows.Application;
namespace WeModPatcher.Core
{
public class StaticPatcher
{
private class PatchEntry
{
public Regex Target { get; set; }
public string Patch { get; set; }
public bool Applied { get; set; }
public bool SingleMatch { get; set; } = true;
public bool DynamicFieldResolve { get; set; }
}
private static readonly Dictionary<EPatchType, PatchEntry> Patches = new Dictionary<EPatchType, PatchEntry>()
{
{
EPatchType.ActivatePro,
new PatchEntry
{
DynamicFieldResolve = true,
Target = new Regex(@"getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?\}\)\}", RegexOptions.Singleline),
Patch = "getUserAccount(){return this.#<fetch_field_name>.fetch({endpoint:\"/v3/account\",method:\"GET\",name:\"/v3/account\",collectMetrics:0}).then(response=>{response.subscription={period:\"yearly\",state:\"active\"};response.flags=78;return response;})}"
}
},
{
EPatchType.DisableUpdates,
new PatchEntry
{
Target = new Regex(@"registerHandler\(""ACTION_CHECK_FOR_UPDATE"".*?\)\)\)\)", RegexOptions.Singleline),
Patch = "registerHandler(\"ACTION_CHECK_FOR_UPDATE\",(e=>expectUpdateFeedUrl(e,(e=>null)))"
}
}
};
private readonly string _weModRootFolder;
private readonly Action<string, ELogType> _logger;
private readonly PatchConfig _config;
private readonly string _asarPath;
private readonly string _backupPath;
private readonly string _unpackedPath;
private int _sumOfPatches = 0;
private readonly string _exePath;
public StaticPatcher(string weModRootFolder, Action<string, ELogType> logger, PatchConfig config)
{
_weModRootFolder = weModRootFolder;
_logger = logger;
_config = config;
_asarPath = Path.Combine(weModRootFolder, "resources", "app.asar");
_unpackedPath = Path.Combine(weModRootFolder, "resources", "app.asar.unpacked");
_backupPath = Path.Combine(weModRootFolder, "resources", "app.asar.backup");
_exePath = Path.Combine(_weModRootFolder, "WeMod.exe");
}
private static string GetFetchFieldName(string targetFunction)
{
var fetchMatch = Regex.Match(targetFunction, @"return\s+this\.#(\w+)\.fetch");
return fetchMatch.Success ? fetchMatch.Groups[1].Value : null;
}
private void ApplyJsPatch(string fileName, string js, PatchEntry patch, EPatchType patchType)
{
if (patch.Applied)
{
return;
}
var matches = patch.Target.Matches(js);
if (matches.Count == 0)
{
return;
}
if(matches.Count > 1 && patch.SingleMatch)
{
throw new Exception(
$"[PATCHER] [{patchType}] Patch failed. Multiple target functions found. Looks like the version is not supported");
}
if (patch.DynamicFieldResolve)
{
string fetchFieldName = GetFetchFieldName(matches[0].Value);
if (string.IsNullOrEmpty(fetchFieldName))
{
throw new Exception($"[PATCHER] [{patchType}] Fetch field name not found");
}
patch.Patch = patch.Patch.Replace("<fetch_field_name>", fetchFieldName);
}
_logger($"[PATCHER] [{patchType}] Found target function in: " + Path.GetFileName(fileName), ELogType.Info);
File.WriteAllText(fileName, patch.Target.Replace(js, patch.Patch));
_logger($"[PATCHER] [{patchType}] Patch applied", ELogType.Success);
patch.Applied = true;
_sumOfPatches -= (int)patchType;
}
private void PatchAsar()
{
var items = Directory.EnumerateFiles(_unpackedPath)
.Where(file => !Directory.Exists(file) && Regex.IsMatch(Path.GetFileName(file), @"^app-\w+|index\.js"))
.ToList();
if (!items.Any())
{
throw new Exception("[PATCHER] No app bundle found");
}
var requestedPatches = _config.PatchTypes.ToList();
requestedPatches.ForEach(patch => _sumOfPatches += (int)patch);
foreach (var item in items)
{
if (_sumOfPatches <= 0)
{
break;
}
string data = File.ReadAllText(item);
foreach (var entry in requestedPatches)
{
ApplyJsPatch(item, data, Patches[entry], entry);
}
}
}
private void PatchPe()
{
_logger("[PATCHER] Patching PE...", ELogType.Info);
var patchResult = MemoryUtils.PatchFile(_exePath,Constants.ExePatchSignature, Constants.ExePatchSignature.PatchBytes);
if(patchResult == -1)
{
_logger("[PATCHER] Failed to patch PE", ELogType.Error);
return;
}
_logger(patchResult == 0 ? "[PATCHER] PE already patched!" : "[PATCHER] PE patched successfully!", ELogType.Success);
}
private void CreateShortcut()
{
// invoke file dialog save file
var fileDialog = new SaveFileDialog()
{
CheckPathExists = true,
AddExtension = true,
SupportMultiDottedExtensions = false,
FileName = "WeMod",
};
if(fileDialog.ShowDialog() != DialogResult.OK)
{
return;
}
_config.Path = _weModRootFolder;
var json = JsonConvert.SerializeObject(_config, Formatting.None);
Utils.Win32.Shortcut.CreateShortcut(
fileName: fileDialog.FileName + ".lnk",
targetPath: Assembly.GetExecutingAssembly().Location,
arguments: Extensions.Base64Encode(json),
workingDirectory: Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
description: null,
iconPath: _exePath
);
_logger("[PATCHER] The shortcut has been created, now you should only run WeMod through this shortcut", ELogType.Success);
}
public void Patch()
{
RuntimePatcher.KillWeMod();
if (!File.Exists(_backupPath))
{
_logger("[PATCHER] Creating backup...", ELogType.Info);
File.Copy(_asarPath, _backupPath);
}
else
{
_logger("[PATCHER] Backup already exists", ELogType.Warn);
}
if(!File.Exists(_asarPath))
{
throw new Exception("app.asar not found");
}
try
{
_logger("[PATCHER] Extracting app.asar...", ELogType.Info);
AsarExtractor.ExtractAll(_asarPath, _unpackedPath);
}
catch (Exception e)
{
throw new Exception($"[PATCHER] Failed to unpack app.asar: {e.Message}");
}
PatchAsar();
try
{
new AsarCreator(_unpackedPath, _asarPath, new CreateOptions
{
Unpack = new Regex(@"^static\\unpacked.*$")
}).CreatePackageWithOptions();
}
catch (Exception e)
{
throw new Exception($"[PATCHER] Failed to pack app.asar: {e.Message}");
}
if (_config.PatchMethod == EPatchProcessMethod.Static)
{
PatchPe();
}
else if(_config.PatchMethod == EPatchProcessMethod.Runtime)
{
Application.Current.Dispatcher.Invoke(CreateShortcut);
}
_logger("[PATCHER] Done!", ELogType.Success);
}
}
}
-26
View File
@@ -1,26 +0,0 @@
using System.Collections.Generic;
namespace WeModPatcher.Models
{
public enum EPatchType
{
ActivatePro = 1,
DisableUpdates = 2,
DisableTelemetry = 4
}
public enum EPatchProcessMethod
{
None = 0,
Runtime = 1,
Static = 2
}
public sealed class PatchConfig
{
public HashSet<EPatchType> PatchTypes { get; set; }
public EPatchProcessMethod PatchMethod { get; set; }
public string Path { get; set; }
}
}
-25
View File
@@ -1,25 +0,0 @@
using WeModPatcher.Utils;
namespace WeModPatcher.Models
{
public sealed class Signature
{
public readonly byte[] OriginalBytes;
public readonly byte[] PatchBytes;
public readonly byte[] Sequence;
public readonly byte[] Mask;
public readonly int Offset;
public int Length => Sequence.Length;
public static implicit operator byte[](Signature signature) => signature.Sequence;
public Signature(string signature, int offset, byte[] patchBytes, byte[] originalBytes)
{
MemoryUtils.ParseSignature(signature, out Sequence, out Mask);
PatchBytes = patchBytes;
OriginalBytes = originalBytes;
Offset = offset;
}
}
}
-178
View File
@@ -1,178 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using WeModPatcher.Models;
using WeModPatcher.Utils.Win32;
namespace WeModPatcher.Utils
{
public class MemoryUtils
{
public static int ScanMemoryBlock(byte[] buffer, int bufferLength, byte[] pattern, byte[] mask)
{
var patternLength = pattern.Length;
if (bufferLength < patternLength)
{
return -1;
}
// Make a length of length outside the first cycle for optimization
var searchEnd = bufferLength - patternLength;
// first pass - use the first non-empty byte of the mask for a quick check
var firstValidIndex = -1;
for (var i = 0; i < patternLength; i++)
{
if (mask[i] == 1)
{
firstValidIndex = i;
break;
}
}
if (firstValidIndex == -1)
{
return 0;
}
var firstByte = pattern[firstValidIndex];
for (var i = 0; i <= searchEnd; i++)
{
// quick check by the first byte before full comparison
if (buffer[i + firstValidIndex] != firstByte)
continue;
var found = true;
// check only those positions where mask = 1
for (var j = 0; j < patternLength; j++)
{
if (mask[j] == 0 || buffer[i + j] == pattern[j])
{
continue;
}
found = false;
break;
}
if (found)
{
return i;
}
}
return -1;
}
public static void ParseSignature(string signatureStr, out byte[] pattern, out byte[] mask)
{
var parts = signatureStr.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
var length = parts.Length;
pattern = new byte[length];
mask = new byte[length];
for (var i = 0; i < length; i++)
{
if (parts[i] == "??" || parts[i] == "?")
{
pattern[i] = 0;
// wildcard byte
mask[i] = 0;
continue;
}
pattern[i] = Convert.ToByte(parts[i], 16);
mask[i] = 1;
}
}
public static bool SafeWriteVirtualMemory(IntPtr hProcess, IntPtr address, byte[] bytes)
{
if (!Imports.VirtualProtectEx(hProcess, address, (IntPtr)1, 0x40, out uint oldProtect))
{
return false;
}
bool result = Imports.WriteProcessMemory(hProcess, address, bytes, bytes.Length, out _);
// Restore the previous access rights
Imports.VirtualProtectEx(hProcess, address, (IntPtr)1, oldProtect, out _);
return result;
}
public static IntPtr ScanVirtualMemory(IntPtr hProcess, IntPtr startAddress, int searchSize, byte[] signature, byte[] mask)
{
const int BUFFER_SIZE = 4096;
byte[] buffer = new byte[BUFFER_SIZE];
// We can't copy all the crap of the process into a byte array at once. Don't try this
for (long currentAddress = startAddress.ToInt64();
currentAddress < startAddress.ToInt64() + searchSize;
currentAddress += BUFFER_SIZE - signature.Length)
{
if (!Imports.ReadProcessMemory(hProcess, new IntPtr(currentAddress), buffer, BUFFER_SIZE, out int bytesRead) || bytesRead == 0)
{
// Read error or end of memory, throw mb?
continue;
}
var i = ScanMemoryBlock(buffer, bytesRead, signature, mask);
if (i != -1)
{
return new IntPtr(currentAddress + i);
}
}
return IntPtr.Zero;
}
public static int PatchFile(string filePath, Signature signature, byte[] patchBytes)
{
const int bufferSize = 8192;
var buffer = new byte[bufferSize + signature.Length - 1];
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
{
int filePosition = 0;
while (true)
{
int bytesRead = fileStream.Read(buffer, 0, bufferSize);
if (bytesRead == 0) break;
int matchIndex = ScanMemoryBlock(buffer, bytesRead, signature, signature.Mask);
if (matchIndex != -1)
{
int functionStartPosition = filePosition + matchIndex;
var checkBuffer = new byte[patchBytes.Length];
fileStream.Seek(functionStartPosition + signature.Offset, SeekOrigin.Begin);
fileStream.Read(checkBuffer, 0, patchBytes.Length);
if (checkBuffer.SequenceEqual(patchBytes))
{
return 0; // Memory already patched
}
// Go to patch position
fileStream.Seek(functionStartPosition + signature.Offset, SeekOrigin.Begin);
fileStream.Write(patchBytes, 0, patchBytes.Length);
return functionStartPosition; // Return the address of the function start by signature
}
filePosition += bytesRead;
Array.Copy(buffer, bufferSize, buffer, 0, signature.Length - 1);
}
}
return -1;
}
}
}
-260
View File
@@ -1,260 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace WeModPatcher.Utils.Win32
{
public static class Imports
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
[Out] byte[] lpBuffer,
int dwSize,
out int lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int nSize,
out IntPtr lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualProtectEx(
IntPtr hProcess,
IntPtr lpAddress,
IntPtr dwSize,
uint flNewProtect,
out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WaitForDebugEvent(ref DEBUG_EVENT lpDebugEvent, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DebugActiveProcessStop(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DebugActiveProcess(int dwProcessId);
[DllImport("psapi.dll", SetLastError = true)]
public static extern bool EnumProcessModules(
IntPtr hProcess,
IntPtr lphModule,
uint cb,
out uint lpcbNeeded);
[DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int GetModuleFileNameEx(
IntPtr hProcess,
IntPtr hModule,
StringBuilder lpFilename,
int nSize);
[DllImport("psapi.dll", SetLastError = true)]
public static extern bool GetModuleInformation(IntPtr hProcess, IntPtr hModule, out MODULEINFO lpmodinfo, uint cb);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern bool CreateProcessA
(
String lpApplicationName,
String lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
Boolean bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
String lpCurrentDirectory,
[In] ref StartupInfo lpStartupInfo,
out ProcessInformation lpProcessInformation
);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ContinueDebugEvent(uint dwProcessId, uint dwThreadId, uint dwContinueStatus);
[StructLayout(LayoutKind.Sequential)]
public struct StartupInfo
{
public Int32 cb ;
public IntPtr lpReserved ;
public IntPtr lpDesktop ;
public IntPtr lpTitle ;
public Int32 dwX ;
public Int32 dwY ;
public Int32 dwXSize ;
public Int32 dwYSize ;
public Int32 dwXCountChars ;
public Int32 dwYCountChars ;
public Int32 dwFillAttribute ;
public Int32 dwFlags ;
public Int16 wShowWindow ;
public Int16 cbReserved2 ;
public IntPtr lpReserved2 ;
public IntPtr hStdInput ;
public IntPtr hStdOutput ;
public IntPtr hStdError ;
}
[StructLayout(LayoutKind.Sequential)]
public struct ProcessInformation
{
public IntPtr hProcess;
public IntPtr hThread;
public Int32 dwProcessId;
public Int32 dwThreadId;
}
#region Debug event structures
[StructLayout(LayoutKind.Explicit)]
public struct DEBUG_EVENT
{
[FieldOffset(0)]
public uint dwDebugEventCode;
[FieldOffset(4)]
public uint dwProcessId;
[FieldOffset(8)]
public uint dwThreadId;
[FieldOffset(16)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 160)]
public byte[] Union;
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct EXCEPTION_DEBUG_INFO
{
public EXCEPTION_RECORD ExceptionRecord;
public uint dwFirstChance;
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct EXCEPTION_RECORD
{
public uint ExceptionCode;
public uint ExceptionFlags;
public IntPtr pExceptionRecord;
public IntPtr ExceptionAddress;
public uint NumberParameters;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 15)]
public IntPtr[] ExceptionInformation;
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct CREATE_THREAD_DEBUG_INFO
{
public IntPtr hThread;
public IntPtr lpThreadLocalBase;
public IntPtr lpStartAddress;
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct CREATE_PROCESS_DEBUG_INFO
{
public IntPtr hFile;
public IntPtr hProcess;
public IntPtr hThread;
public IntPtr lpBaseOfImage;
public uint dwDebugInfoFileOffset;
public uint nDebugInfoSize;
public IntPtr lpThreadLocalBase;
public IntPtr lpStartAddress;
public IntPtr lpImageName;
public ushort fUnicode;
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct MODULEINFO
{
public IntPtr lpBaseOfDll;
public uint SizeOfImage;
public IntPtr EntryPoint;
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct EXIT_THREAD_DEBUG_INFO
{
public uint dwExitCode;
}
[StructLayout(LayoutKind.Sequential)]
public struct EXIT_PROCESS_DEBUG_INFO
{
public uint dwExitCode;
}
[StructLayout(LayoutKind.Sequential)]
public struct LOAD_DLL_DEBUG_INFO
{
public IntPtr hFile;
public IntPtr lpBaseOfDll;
public uint dwDebugInfoFileOffset;
public uint nDebugInfoSize;
public IntPtr lpImageName;
public ushort fUnicode;
}
[StructLayout(LayoutKind.Sequential)]
public struct UNLOAD_DLL_DEBUG_INFO
{
public IntPtr lpBaseOfDll;
}
[StructLayout(LayoutKind.Sequential)]
public struct OUTPUT_DEBUG_STRING_INFO
{
public IntPtr lpDebugStringData;
public ushort fUnicode;
public ushort nDebugStringLength;
}
[StructLayout(LayoutKind.Sequential)]
public struct RIP_INFO
{
public uint dwError;
public uint dwType;
}
public static T MapUnmanagedStructure<T>(byte[] debugInfo)
{
GCHandle handle = GCHandle.Alloc(debugInfo, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
}
finally
{
handle.Free();
}
}
#endregion
// Determining constants for debugging
public const uint INFINITE = 0xFFFFFFFF;
public const uint DEBUG_PROCESS = 0x00000001;
public const uint DBG_CONTINUE = 0x00010002;
public const uint CREATE_PROCESS_DEBUG_EVENT = 3;
public const uint EXIT_PROCESS_DEBUG_EVENT = 5;
public const uint EXCEPTION_DEBUG_EVENT = 1;
public const uint LOAD_DLL_DEBUG_EVENT = 6;
public const uint OUTPUT_DEBUG_STRING_EVENT = 8;
public const uint EXCEPTION_BREAKPOINT = 0x80000003;
public const uint DBG_EXCEPTION_NOT_HANDLED = 0x80010001;
// Constants for VirtualProtectex
public const uint PAGE_EXECUTE_READWRITE = 0x40;
}
}
@@ -1,126 +0,0 @@
<UserControl x:Class="WeModPatcher.View.Popups.PatchVectorsPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WeModPatcher.View.Popups"
xmlns:controls="clr-namespace:WeModPatcher.View.Controls"
mc:Ignorable="d"
d:DesignHeight="Auto" d:DesignWidth="Auto"
Background="{DynamicResource Background}"
Foreground="{DynamicResource MutedForeground}"
FontWeight="Medium"
FontSize="13">
<UserControl.Resources>
<Button x:Key="BackButton" Click="BackClicked" VerticalAlignment="Bottom" Padding="3"
Margin="0 0 15 0"
Width="35" Height="23" Style="{StaticResource IconButton}"
Tag="{StaticResource ArrowLeft}"/>
</UserControl.Resources>
<Grid>
<Grid x:Name="PatchVectors" Visibility="Visible" Margin="0 0 5 0">
<Grid.RowDefinitions>
<RowDefinition Height="27"/>
<RowDefinition Height="27"/>
<RowDefinition Height="27"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" VerticalAlignment="Center" Text="Activate WeMod Pro"/>
<CheckBox Grid.Row="0" x:Name="ActivateProBox" HorizontalAlignment="Right" VerticalAlignment="Center" IsChecked="True"/>
<TextBlock Grid.Row="1" VerticalAlignment="Center" Text="Disable telemetry"/>
<CheckBox Grid.Row="1" x:Name="DisableTelemetryBox" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" VerticalAlignment="Center" Text="Disable updates"/>
<CheckBox Grid.Row="2" x:Name="DisableUpdateBox" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<Button Grid.Row="3" Padding="0 5 0 5" Margin="0 15 0 0" Content="Continue"
Click="NextClicked"/>
</Grid>
<Grid x:Name="PatchMethod" Visibility="Collapsed" Width="650">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid>
<Border Background="{DynamicResource Muted}" HorizontalAlignment="Right" Width="2"
CornerRadius="10"/>
<StackPanel>
<TextBlock FontSize="16" Text="Static" Foreground="{DynamicResource Foreground}" HorizontalAlignment="Center" Margin="0 0 0 10"/>
<controls:InfoItem
IconColor="SpringGreen"
IconData="{StaticResource CheckDecagram}"
Text="Starting WeMod without this program" />
<controls:InfoItem
Margin="0 15 0 0"
IconColor="PaleVioletRed"
IconData="{StaticResource AlertDecagram}"
Text="Violation of WeMod digital signature (possibly marked by antiviruses, anti-cheats)" />
<controls:InfoItem
Margin="0 10 0 0"
IconColor="PaleVioletRed"
IconData="{StaticResource AlertDecagram}"
Text="Auto-patching after WeMod updates is not available" />
<controls:InfoItem
Margin="0 10 0 0"
IconColor="PaleVioletRed"
IconData="{StaticResource AlertDecagram}"
Text="Hotkeys will be broken" />
</StackPanel>
</Grid>
<Grid Grid.Row="0" Grid.Column="1">
<StackPanel Margin="10 0 0 0">
<TextBlock FontSize="16" Text="Runtime" Foreground="{DynamicResource Foreground}"
HorizontalAlignment="Center" Margin="-10 0 0 10"/>
<controls:InfoItem
IconColor="SpringGreen"
IconData="{StaticResource CheckDecagram}"
Text="Hotkeys still work" />
<controls:InfoItem Margin="0 10 0 0"
IconColor="SpringGreen"
IconData="{StaticResource CheckDecagram}"
Text="Does not break the digital signature (does not make changes to the original .exe)" />
<controls:InfoItem Margin="0 10 0 0"
IconColor="SpringGreen"
IconData="{StaticResource CheckDecagram}"
Text="Automatically applies patches to new versions (referring to your current selection)" />
<controls:InfoItem
Margin="0 10 0 0"
IconColor="Yellow"
IconData="{StaticResource AlertDecagram}"
Text="The WeMod startup process is controlled by the patcher. (Don't worry, you will no longer see this window. You will run WeMod as usual but using the shortcut that will be created after choosing this method). So you will want to keep this program. Make sure it's in a safe directory (not Temp, Downloads, etc)." />
<controls:InfoItem Margin="0 10 0 0"
IconColor="PaleVioletRed"
IconData="{StaticResource AlertDecagram}"
Text="Running WeMod directly through official WeMod.exe is not possible until you restore patch backup" />
</StackPanel>
</Grid>
<Button Grid.Column="0" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Center"
Padding="10 5" Margin="0 15 0 0"
Click="OnStaticSelected"
Content="Use static"/>
<Button Grid.Column="1" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Center"
Padding="10 5" Margin="0 15 0 0"
Click="OnRuntimeSelected"
Content="Use runtime"/>
</Grid>
</Grid>
</UserControl>
@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using WeModPatcher.Models;
using WeModPatcher.View.Controls;
namespace WeModPatcher.View.Popups
{
public partial class PatchVectorsPopup : UserControl, IDisposable
{
private readonly Action<PatchConfig> _onApply;
private readonly StackPanel _popupTitleContainer;
private string _originalTitle;
private readonly TextBlock _titleTextBlock;
public PatchVectorsPopup(Action<PatchConfig> onApply)
{
_onApply = onApply;
InitializeComponent();
_popupTitleContainer = MainWindow.MainWindow.Instance.PopupHost.TitleContainer;
_titleTextBlock = _popupTitleContainer.Children[0] as TextBlock;
}
private void BackClicked(object sender, RoutedEventArgs e)
{
Dispose();
PatchMethod.Visibility = Visibility.Collapsed;
PatchVectors.Visibility = Visibility.Visible;
}
private void OnRuntimeSelected(object sender, RoutedEventArgs e)
=> RaiseCallback(EPatchProcessMethod.Runtime);
private void OnStaticSelected(object sender, RoutedEventArgs e)
=> RaiseCallback(EPatchProcessMethod.Static);
private void RaiseCallback(EPatchProcessMethod method)
{
if (ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true &&
DisableTelemetryBox.IsChecked != true)
{
return;
}
var result = new HashSet<EPatchType>();
if (ActivateProBox.IsChecked == true)
{
result.Add(EPatchType.ActivatePro);
}
if (DisableUpdateBox.IsChecked == true)
{
result.Add(EPatchType.DisableUpdates);
}
_onApply(new PatchConfig
{
PatchTypes = result,
PatchMethod = method
});
}
private void NextClicked(object sender, RoutedEventArgs e)
{
_popupTitleContainer.Children.Insert(0, FindResource("BackButton") as Button);
_originalTitle = _titleTextBlock.Text;
_titleTextBlock.Text = "Patch method";
PatchMethod.Visibility = Visibility.Visible;
PatchVectors.Visibility = Visibility.Collapsed;
}
public void Dispose()
{
if (PatchVectors.Visibility == Visibility.Collapsed)
{
_popupTitleContainer.Children.RemoveAt(0);
_titleTextBlock.Text = _originalTitle;
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+78
View File
@@ -0,0 +1,78 @@
# Build directories
/build/
/build-debug/
/build-release/
/out/
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
CTestTestfile.cmake
Makefile
install_manifest.txt
# Compiled binaries
*.o
*.obj
*.lo
*.la
*.a
*.so
*.so.*
*.dylib
*.dll
*.exe
*.out
*.app
# Debug files
*.pch
*.pdb
*.mod
*.map
# Generated configuration headers
config.h
config.hpp
# Logs
*.log
# IDE files
# VS Code
.vscode/
*.code-workspace
# CLion
.idea/
# Visual Studio
*.user
*.suo
*.vcxproj.user
*.vcxproj.*
*.sln
# Xcode
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
*.xcworkspace/
xcuserdata/
# OS junk
# macOS
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Backup files
*~
*.swp
*.tmp
+15
View File
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.16)
project(asar_fuses_bypass C)
set(CMAKE_C_STANDARD 11)
#[[
add_executable(asar_fuses_bypass main.c)
]]
set(CMAKE_SHARED_LIBRARY_PREFIX "")
set(CMAKE_STATIC_LIBRARY_PREFIX "")
add_link_options(-static -static-libgcc -static-libstdc++)
add_library(version SHARED library.c library.def fuses.c)
+190
View File
@@ -0,0 +1,190 @@
//
// Created by kitbyte on 30.11.2025.
//
#include <Windows.h>
#include <stdio.h>
#include <time.h>
#define ENABLE_LOGGING 0
#ifndef _DEBUG
#undef ENABLE_LOGGING
#define ENABLE_LOGGING 0
#endif
#define FUSE_SENTINEL_LENGTH 32
#define FUSE_VERSION_SUPPORTED 1
#define FUSE_MIN_WIRE_LENGTH 5
#define ALIGN8(ptr, mod) ((((ULONG_PTR)(ptr) + 7) & ~7) + ((mod) * 8))
#if defined(_WIN64)
#define SENTINEL_PART1 0x6E64474B70374C64ULL
#define SENTINEL_PART2 0x6262503639377A4EULL
#define SENTINEL_PART3 0x58486D4B4E57516AULL
#define SENTINEL_PART4 0x5873743942615A42ULL
#else
static const DWORD SENTINEL_PARTS[8] = {
0x70374C64, 0x6E64474B,
0x39377A4E, 0x62625036,
0x4E57516A, 0x58486D4B,
0x42615A42, 0x58737439
};
#endif
typedef enum {
FUSE_RUN_AS_NODE = 0,
FUSE_COOKIE_ENCRYPTION = 1,
FUSE_NODE_OPTIONS = 2,
FUSE_NODE_CLI_INSPECT = 3,
FUSE_ASAR_INTEGRITY_VALIDATION = 4,
FUSE_ONLY_LOAD_APP_FROM_ASAR = 5,
FUSE_LOAD_BROWSER_V8_SNAPSHOT = 6,
FUSE_GRANT_FILE_PROTOCOL = 7
} ElectronFuseIndex;
typedef enum {
FUSE_STATE_DISABLED = '0',
FUSE_STATE_ENABLED = '1',
FUSE_STATE_REMOVED = 'r'
} FuseState;
typedef struct {
char sentinel[FUSE_SENTINEL_LENGTH];
unsigned char version;
unsigned char wire_length;
unsigned char fuses[];
} FuseWire;
#if ENABLE_LOGGING
static FILE* g_logFile = NULL;
static void log_init(void) {
char path[MAX_PATH];
GetModuleFileNameA(NULL, path, MAX_PATH);
char* dot = strrchr(path, '.');
if (dot) strcpy(dot, ".log");
else strcat(path, ". log");
g_logFile = fopen(path, "a");
if (g_logFile) {
time_t now = time(NULL);
fprintf(g_logFile, "\n=== Session: %s", ctime(&now));
fflush(g_logFile);
}
}
static void log_close(void) {
if (g_logFile) {
fclose(g_logFile);
g_logFile = NULL;
}
}
static void log_msg(const char* fmt, .. .) {
if (!g_logFile) return;
va_list args;
va_start(args, fmt);
vfprintf(g_logFile, fmt, args);
va_end(args);
fflush(g_logFile);
}
#else
#define log_init() ((void)0)
#define log_close() ((void)0)
#define log_msg(...) ((void)0)
#endif
static FuseWire* find_fuse_wire(int offset) {
char* base = (char*)GetModuleHandleA(NULL);
if (!base) return NULL;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return NULL;
DWORD size = nt->OptionalHeader.SizeOfImage;
char* start = (char*)ALIGN8(base, 1) + offset;
char* end = (char*)ALIGN8(base + size - FUSE_SENTINEL_LENGTH, -1) - offset;
#if defined(_WIN64)
for (DWORD64* p = (DWORD64*)start; p < (DWORD64*)end; p++) {
if (p[0] == SENTINEL_PART1 && p[1] == SENTINEL_PART2 &&
p[2] == SENTINEL_PART3 && p[3] == SENTINEL_PART4) {
log_msg("[+] Sentinel at: %p\n", p);
return (FuseWire*)p;
}
}
#else
for (DWORD* p = (DWORD*)start; p < (DWORD*)end; p += 2) {
if (p[0] == SENTINEL_PARTS[0] && p[1] == SENTINEL_PARTS[1] &&
p[2] == SENTINEL_PARTS[2] && p[3] == SENTINEL_PARTS[3] &&
p[4] == SENTINEL_PARTS[4] && p[5] == SENTINEL_PARTS[5] &&
p[6] == SENTINEL_PARTS[6] && p[7] == SENTINEL_PARTS[7]) {
log_msg("[+] Sentinel at: %p\n", p);
return (FuseWire*)p;
}
}
#endif
return NULL;
}
static BOOL patch_fuse(unsigned char* fuse) {
DWORD prot;
if (!VirtualProtect(fuse, 1, PAGE_READWRITE, &prot)) {
log_msg("[-] VirtualProtect failed: %lu\n", GetLastError());
return FALSE;
}
*fuse = FUSE_STATE_REMOVED;
VirtualProtect(fuse, 1, prot, &prot);
return TRUE;
}
BOOL disable_asar_integrity(void) {
log_init();
FuseWire* wire = find_fuse_wire(0);
if (! wire) wire = find_fuse_wire(4);
if (! wire) {
log_msg("[-] Fuse wire not found\n");
log_close();
return FALSE;
}
log_msg("[+] Wire at %p, ver=%d, len=%d\n", wire, wire->version, wire->wire_length);
if (wire->version != FUSE_VERSION_SUPPORTED) {
log_msg("[-] Unsupported version: %d\n", wire->version);
log_close();
return FALSE;
}
if (wire->wire_length < FUSE_MIN_WIRE_LENGTH) {
log_msg("[*] Wire too short, skip\n");
log_close();
return TRUE;
}
unsigned char* target = &wire->fuses[FUSE_ASAR_INTEGRITY_VALIDATION];
if (*target == FUSE_STATE_REMOVED) {
log_msg("[*] Already patched\n");
log_close();
return TRUE;
}
log_msg("[*] Patching fuse[%d]: 0x%02X -> 0x%02X\n",
FUSE_ASAR_INTEGRITY_VALIDATION, *target, FUSE_STATE_REMOVED);
BOOL result = patch_fuse(target);
log_msg(result ? "[+] Success\n" : "[-] Failed\n");
log_close();
return result;
}
+88
View File
@@ -0,0 +1,88 @@
//
// Created by kitbyte on 30.11.2025.
//
#include <Windows.h>
extern BOOL disable_asar_integrity(void);
#ifdef _WIN64
#define WRAPPER_GENFUNC(name) \
FARPROC orig_##name; \
void _##name(); \
__asm__( \
".global _" #name "\n" \
"_" #name ":\n" \
" movq orig_" #name "(%rip), %rax\n" \
" jmp *%rax\n" \
);
#else
#define WRAPPER_GENFUNC(name) \
FARPROC orig_##name; \
__declspec(naked) void _##name() \
{ \
asm("jmp *_orig_"#name); \
}
#endif
WRAPPER_GENFUNC(GetFileVersionInfoA)
WRAPPER_GENFUNC(GetFileVersionInfoByHandle)
WRAPPER_GENFUNC(GetFileVersionInfoExW)
WRAPPER_GENFUNC(GetFileVersionInfoExA)
WRAPPER_GENFUNC(GetFileVersionInfoSizeA)
WRAPPER_GENFUNC(GetFileVersionInfoSizeExA)
WRAPPER_GENFUNC(GetFileVersionInfoSizeExW)
WRAPPER_GENFUNC(GetFileVersionInfoSizeW)
WRAPPER_GENFUNC(GetFileVersionInfoW)
WRAPPER_GENFUNC(VerFindFileA)
WRAPPER_GENFUNC(VerFindFileW)
WRAPPER_GENFUNC(VerInstallFileA)
WRAPPER_GENFUNC(VerInstallFileW)
WRAPPER_GENFUNC(VerLanguageNameA)
WRAPPER_GENFUNC(VerLanguageNameW)
WRAPPER_GENFUNC(VerQueryValueA)
WRAPPER_GENFUNC(VerQueryValueW)
#define WRAPPER_FUNC(name) orig_##name = GetProcAddress(hOriginalDll, #name);
void SourceInit()
{
TCHAR source[MAX_PATH];
GetSystemDirectory(source, MAX_PATH);
strcat_s(source, sizeof source, "\\version.dll");
HMODULE hOriginalDll = LoadLibrary(source);
WRAPPER_FUNC(GetFileVersionInfoA);
WRAPPER_FUNC(GetFileVersionInfoByHandle);
WRAPPER_FUNC(GetFileVersionInfoExW);
WRAPPER_FUNC(GetFileVersionInfoExA);
WRAPPER_FUNC(GetFileVersionInfoSizeA);
WRAPPER_FUNC(GetFileVersionInfoSizeExW);
WRAPPER_FUNC(GetFileVersionInfoSizeExA);
WRAPPER_FUNC(GetFileVersionInfoSizeW);
WRAPPER_FUNC(GetFileVersionInfoW);
WRAPPER_FUNC(VerFindFileA);
WRAPPER_FUNC(VerFindFileW);
WRAPPER_FUNC(VerInstallFileA);
WRAPPER_FUNC(VerInstallFileW);
WRAPPER_FUNC(VerLanguageNameA);
WRAPPER_FUNC(VerLanguageNameW);
WRAPPER_FUNC(VerQueryValueA);
WRAPPER_FUNC(VerQueryValueW);
}
void Payload()
{
disable_asar_integrity();
}
BOOL WINAPI DllMain(HMODULE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hinstDLL);
SourceInit();
Payload();
}
return TRUE;
}
+20
View File
@@ -0,0 +1,20 @@
LIBRARY "VERSION"
EXPORTS
GetFileVersionInfoA = _GetFileVersionInfoA
GetFileVersionInfoByHandle = _GetFileVersionInfoByHandle
GetFileVersionInfoExA = _GetFileVersionInfoExA
GetFileVersionInfoExW = _GetFileVersionInfoExW
GetFileVersionInfoSizeA = _GetFileVersionInfoSizeA
GetFileVersionInfoSizeExA = _GetFileVersionInfoSizeExA
GetFileVersionInfoSizeExW = _GetFileVersionInfoSizeExW
GetFileVersionInfoSizeW = _GetFileVersionInfoSizeW
GetFileVersionInfoW = _GetFileVersionInfoW
VerFindFileA = _VerFindFileA
VerFindFileW = _VerFindFileW
VerInstallFileA = _VerInstallFileA
VerInstallFileW = _VerInstallFileW
VerLanguageNameA = _VerLanguageNameA
VerLanguageNameW = _VerLanguageNameW
VerQueryValueA = _VerQueryValueA
VerQueryValueW = _VerQueryValueW
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+7
View File
@@ -0,0 +1,7 @@
node_modules/
coverage/
.pnpm-store/
pnpm-lock.yaml
package-lock.json
pnpm-lock.yaml
yarn.lock
+11
View File
@@ -0,0 +1,11 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "src/index.css",
"tailwindFunctions": ["cn", "cva"]
}
+31
View File
@@ -0,0 +1,31 @@
# Wand Web Panel
Local mobile-friendly web panel scaffold for Wand.
## Commands
```bash
npm install
npm run dev
```
Hosted access on the local machine:
- `http://localhost:4173/?mock=1`
Hosted access on the LAN:
```bash
npm run dev:host
```
Then open the machine IP on port `4173`.
## Modes
- `?mock=1`
- dev server only; loads the demo trainer and values through a debug-only import
- `?ws=ws://host:port/remote/ws`
- connects to a real bridge once the desktop layer exists
Production builds exclude the debug route and demo JSON from the shipped bundle.
+212
View File
@@ -0,0 +1,212 @@
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { WebSocketServer } from 'ws';
import demoSession from '../fixtures/demo-session.json' with { type: 'json' };
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
const distDir = path.join(rootDir, 'dist');
const DEFAULT_REMOTE_PORT = 3223;
const DEFAULT_REMOTE_HOST = '0.0.0.0';
const REMOTE_BASE_PATH = '/remote/';
const REMOTE_WS_PATH = '/remote/ws';
const REMOTE_HEALTH_PATH = '/remote/api/health';
const REMOTE_ASSETS_PREFIX = '/remote/assets/';
const host = process.env.HOST || DEFAULT_REMOTE_HOST;
const port = Number(process.env.PORT || DEFAULT_REMOTE_PORT);
const trainerMeta = structuredClone(demoSession.trainerMeta);
const trainerValues = structuredClone(demoSession.trainerValues);
const wss = new WebSocketServer({ noServer: true });
function jsonMessage(type, payload, requestId = null) {
return JSON.stringify({
type,
version: 1,
requestId,
payload,
});
}
function sendSnapshot(ws) {
ws.send(
jsonMessage('trainer_meta', trainerMeta)
);
ws.send(
jsonMessage('trainer_values', trainerValues)
);
}
function broadcast(type, payload, requestId = null) {
const serialized = jsonMessage(type, payload, requestId);
for (const client of wss.clients) {
if (client.readyState === 1) {
client.send(serialized);
}
}
}
function normalizeValue(target, value) {
const cheat = trainerMeta.schema.cheats.find((entry) => entry.target === target);
if (!cheat) {
return value;
}
if (cheat.type === 'toggle') {
return Boolean(value);
}
if (cheat.type === 'slider' || cheat.type === 'number') {
const numeric = typeof value === 'string' ? Number(value) : value;
return Number.isFinite(numeric) ? numeric : trainerValues.values[target];
}
return value;
}
function contentTypeFor(filePath) {
if (filePath.endsWith('.html')) return 'text/html; charset=utf-8';
if (filePath.endsWith('.js')) return 'application/javascript; charset=utf-8';
if (filePath.endsWith('.css')) return 'text/css; charset=utf-8';
if (filePath.endsWith('.json')) return 'application/json; charset=utf-8';
if (filePath.endsWith('.svg')) return 'image/svg+xml';
return 'application/octet-stream';
}
async function serveFile(res, filePath) {
try {
const content = await readFile(filePath);
res.writeHead(200, {
'Content-Type': contentTypeFor(filePath),
'Cache-Control': 'no-store',
});
res.end(content);
} catch {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not found');
}
}
const server = createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
if (url.pathname === '/' || url.pathname === '') {
res.writeHead(302, { Location: REMOTE_BASE_PATH });
res.end();
return;
}
if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) {
res.writeHead(302, { Location: REMOTE_BASE_PATH });
res.end();
return;
}
if (url.pathname === REMOTE_BASE_PATH) {
await serveFile(res, path.join(distDir, 'index.html'));
return;
}
if (url.pathname === REMOTE_HEALTH_PATH) {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ ok: true, trainerId: trainerMeta.trainer.trainerId }));
return;
}
if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) {
const relativePath = url.pathname.replace(REMOTE_BASE_PATH, '');
await serveFile(res, path.join(distDir, relativePath));
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not found');
});
server.on('upgrade', (request, socket, head) => {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
if (url.pathname !== REMOTE_WS_PATH) {
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
wss.on('connection', (ws) => {
ws.on('error', console.error);
ws.on('message', (raw) => {
try {
const message = JSON.parse(String(raw));
if (message?.type === 'hello') {
ws.send(
jsonMessage('hello_ack', {
sessionId: `sess_${Date.now()}`,
accepted: true,
serverVersion: '0.1.0-demo',
protocolVersion: 1,
}, message.requestId ?? null)
);
sendSnapshot(ws);
return;
}
if (message?.type === 'set_value') {
const target = message.payload?.target;
if (typeof target !== 'string' || !(target in trainerValues.values)) {
ws.send(
jsonMessage('set_value_result', {
ok: false,
trainerId: trainerMeta.trainer.trainerId,
target: typeof target === 'string' ? target : '',
error: {
code: 'invalid_target',
message: 'Unknown cheat target.',
},
}, message.requestId ?? null)
);
return;
}
const previousValue = trainerValues.values[target];
const nextValue = normalizeValue(target, message.payload?.value);
trainerValues.values[target] = nextValue;
ws.send(
jsonMessage('set_value_result', {
ok: true,
trainerId: trainerMeta.trainer.trainerId,
target,
}, message.requestId ?? null)
);
broadcast('value_changed', {
trainerId: trainerMeta.trainer.trainerId,
target,
value: nextValue,
oldValue: previousValue,
source: 'remote',
cheatId: message.payload?.cheatId,
});
}
} catch (error) {
ws.send(
jsonMessage('error', {
code: 'invalid_message',
message: error instanceof Error ? error.message : 'Failed to parse client message.',
})
);
}
});
});
server.listen(port, host, () => {
console.log(`Wand web panel bridge listening on http://${host === DEFAULT_REMOTE_HOST ? 'localhost' : host}:${port}${REMOTE_BASE_PATH}`);
});
+896
View File
@@ -0,0 +1,896 @@
const crypto = require('node:crypto');
const fs = require('node:fs');
const http = require('node:http');
const os = require('node:os');
const path = require('node:path');
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const KNOWN_CHEAT_TYPES = new Set(['slider', 'number', 'toggle', 'button', 'selection', 'scalar', 'incremental']);
const DEFAULT_REMOTE_PORT = 3223;
const PORT_SCAN_RANGE = 30;
const DEFAULT_REMOTE_HOST = '0.0.0.0';
const REMOTE_BASE_PATH = '/remote/';
const REMOTE_WS_PATH = '/remote/ws';
const REMOTE_HEALTH_PATH = '/remote/api/health';
const REMOTE_ASSETS_PREFIX = '/remote/assets/';
const BRIDGE_LOG_FILE_NAME = 'wand-remote-bridge.log';
const RENDERER_SCRIPTS_DIR = 'renderer-scripts';
const RENDERER_SCRIPT_API_VERSION = 1;
function isRecord(value) {
return typeof value === 'object' && value !== null;
}
function safeString(value, fallback = '') {
return typeof value === 'string' && value.length ? value : fallback;
}
function firstString(...values) {
for (const value of values) {
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
}
return '';
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map(cloneValue);
}
if (isRecord(value)) {
const result = {};
for (const [key, entry] of Object.entries(value)) {
result[key] = cloneValue(entry);
}
return result;
}
return value;
}
function isValidPort(value) {
return Number.isFinite(value) && value > 0 && value < 65536;
}
function normalizeOption(option) {
if (typeof option === 'string' || typeof option === 'number') {
return {
label: String(option),
value: option,
};
}
if (isRecord(option)) {
const value = option.value;
if (typeof value === 'string' || typeof value === 'number') {
return {
label: safeString(option.label, String(value)),
value,
};
}
}
return null;
}
function normalizeArgs(args) {
if (!isRecord(args)) {
return {};
}
const next = {};
if (typeof args.min === 'number') next.min = args.min;
if (typeof args.max === 'number') next.max = args.max;
if (typeof args.step === 'number') next.step = args.step;
if (typeof args.postfix === 'string') next.postfix = args.postfix;
if (typeof args.default === 'string' || typeof args.default === 'number' || typeof args.default === 'boolean') {
next.default = args.default;
}
if (Array.isArray(args.options)) {
next.options = args.options.map(normalizeOption).filter(Boolean);
}
if (typeof args.button === 'string' || typeof args.button === 'boolean') {
next.button = args.button;
}
return next;
}
function normalizeCheat(cheat, index) {
if (!isRecord(cheat)) {
return null;
}
const target = safeString(cheat.target);
const type = safeString(cheat.type);
if (!target || !KNOWN_CHEAT_TYPES.has(type)) {
return null;
}
const normalized = {
uuid: safeString(cheat.uuid, `${target}-${index}`),
target,
type,
name: safeString(cheat.name, target),
description: typeof cheat.description === 'string' ? cheat.description : null,
instructions: typeof cheat.instructions === 'string' ? cheat.instructions : null,
category: safeString(cheat.category, 'general'),
parent: typeof cheat.parent === 'string' ? cheat.parent : null,
args: normalizeArgs(cheat.args),
};
if (typeof cheat.flags === 'number') {
normalized.flags = cheat.flags;
}
if (Array.isArray(cheat.hotkeys)) {
normalized.hotkeys = cheat.hotkeys.filter(Array.isArray).map((group) => group.map((item) => String(item)));
}
return normalized;
}
function normalizeSnapshot(rawSnapshot) {
if (!isRecord(rawSnapshot) || !isRecord(rawSnapshot.metadata) || !isRecord(rawSnapshot.metadata.info)) {
return null;
}
const info = rawSnapshot.metadata.info;
const blueprint = isRecord(info.blueprint) ? info.blueprint : {};
const rawCheats = Array.isArray(blueprint.cheats) ? blueprint.cheats : [];
const cheats = rawCheats.map(normalizeCheat).filter(Boolean);
const categories = Array.from(new Set(cheats.map((entry) => entry.category)));
const trainerId = safeString(rawSnapshot.trainerId || rawSnapshot.trainerInfo?.trainerId);
const displayName = firstString(
rawSnapshot.trainerInfo?.displayName,
rawSnapshot.trainerInfo?.gameName,
rawSnapshot.trainerInfo?.titleName,
rawSnapshot.trainerInfo?.title,
rawSnapshot.trainerInfo?.name,
info.displayName,
info.gameName,
info.titleName,
info.title,
info.name,
info.game?.displayName,
info.game?.name,
info.game?.title
);
if (!trainerId) {
return null;
}
const trainerMeta = {
session: {
instanceId: safeString(rawSnapshot.instanceId, 'wand-session'),
},
trainer: {
trainerId,
gameId: safeString(rawSnapshot.trainerInfo?.gameId || info.gameId),
displayName: displayName || safeString(rawSnapshot.trainerInfo?.gameId || info.gameId, trainerId),
titleId: typeof info.titleId === 'string' ? info.titleId : null,
gameVersion: typeof rawSnapshot.gameVersion === 'string' ? rawSnapshot.gameVersion : null,
trainerLoading: rawSnapshot.trainerLoading === true,
gameInstalled: rawSnapshot.gameInstalled !== false,
needsCompatibilityWarning: rawSnapshot.needsCompatibilityWarning === true,
language: safeString(rawSnapshot.language, 'en-US'),
themeId: safeString(rawSnapshot.themeId, 'default'),
isTimeLimitExpired: rawSnapshot.isTimeLimitExpired === true,
notesReadHash: typeof rawSnapshot.notesReadHash === 'string' ? rawSnapshot.notesReadHash : null,
},
schema: {
categories,
cheats,
},
};
const trainerValues = {
trainerId,
values: isRecord(rawSnapshot.values) ? cloneValue(rawSnapshot.values) : {},
};
return {
trainerMeta,
trainerValues,
};
}
function jsonMessage(type, payload, requestId = null) {
return JSON.stringify({
type,
version: 1,
requestId,
payload,
});
}
function makeFrame(opcode, payload) {
const source = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
const header = [];
header.push(0x80 | (opcode & 0x0f));
if (source.length < 126) {
header.push(source.length);
return Buffer.concat([Buffer.from(header), source]);
}
if (source.length < 65536) {
const prefix = Buffer.from([header[0], 126, (source.length >> 8) & 0xff, source.length & 0xff]);
return Buffer.concat([prefix, source]);
}
const prefix = Buffer.alloc(10);
prefix[0] = header[0];
prefix[1] = 127;
prefix.writeUInt32BE(0, 2);
prefix.writeUInt32BE(source.length, 6);
return Buffer.concat([prefix, source]);
}
function sendText(client, text) {
if (!client.closed) {
client.socket.write(makeFrame(1, Buffer.from(text, 'utf8')));
}
}
function sendJson(client, type, payload, requestId = null) {
sendText(client, jsonMessage(type, payload, requestId));
}
function closeClient(client, code = 1000, reason = 'Closing') {
if (client.closed) {
return;
}
client.closed = true;
const reasonBuffer = Buffer.from(reason, 'utf8');
const payload = Buffer.alloc(2 + reasonBuffer.length);
payload.writeUInt16BE(code, 0);
reasonBuffer.copy(payload, 2);
client.socket.write(makeFrame(8, payload));
client.socket.end();
}
function parseFrame(buffer) {
if (buffer.length < 2) {
return null;
}
const first = buffer[0];
const second = buffer[1];
const fin = (first & 0x80) !== 0;
const opcode = first & 0x0f;
const masked = (second & 0x80) !== 0;
let length = second & 0x7f;
let offset = 2;
if (length === 126) {
if (buffer.length < offset + 2) {
return null;
}
length = buffer.readUInt16BE(offset);
offset += 2;
} else if (length === 127) {
if (buffer.length < offset + 8) {
return null;
}
const high = buffer.readUInt32BE(offset);
const low = buffer.readUInt32BE(offset + 4);
if (high !== 0) {
throw new Error('Large websocket frames are not supported.');
}
length = low;
offset += 8;
}
let mask = null;
if (masked) {
if (buffer.length < offset + 4) {
return null;
}
mask = buffer.subarray(offset, offset + 4);
offset += 4;
}
if (buffer.length < offset + length) {
return null;
}
const payload = Buffer.from(buffer.subarray(offset, offset + length));
if (masked && mask) {
for (let index = 0; index < payload.length; index += 1) {
payload[index] ^= mask[index % 4];
}
}
return {
bytesConsumed: offset + length,
fin,
opcode,
payload,
};
}
function contentTypeFor(filePath) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
case '.html':
return 'text/html; charset=utf-8';
case '.js':
case '.cjs':
return 'application/javascript; charset=utf-8';
case '.css':
return 'text/css; charset=utf-8';
case '.json':
return 'application/json; charset=utf-8';
case '.svg':
return 'image/svg+xml';
default:
return 'application/octet-stream';
}
}
function getAdvertisedUrls(port) {
const urls = [];
const interfaces = os.networkInterfaces();
for (const entries of Object.values(interfaces)) {
if (!entries) {
continue;
}
for (const entry of entries) {
if (!entry || entry.internal || entry.family !== 'IPv4') {
continue;
}
urls.push(`http://${entry.address}:${port}${REMOTE_BASE_PATH}`);
}
}
urls.unshift(`http://localhost:${port}${REMOTE_BASE_PATH}`);
return Array.from(new Set(urls));
}
function createBridgeRuntime(options = {}) {
const preferredPort = Number(options.port || process.env.WAND_REMOTE_PORT || DEFAULT_REMOTE_PORT);
let port = isValidPort(preferredPort) ? preferredPort : DEFAULT_REMOTE_PORT;
const maxPort = Number(options.maxPort || process.env.WAND_REMOTE_MAX_PORT || port + PORT_SCAN_RANGE);
const host = options.host || process.env.WAND_REMOTE_HOST || DEFAULT_REMOTE_HOST;
const panelRoot = options.panelRoot || __dirname;
const clients = new Set();
let advertisedUrls = [];
let currentSnapshot = null;
let setValueHandler = null;
let listening = false;
function setAdvertisedPort(nextPort) {
port = nextPort;
advertisedUrls = getAdvertisedUrls(port);
globalThis.__wandRemoteBridgeUrl = advertisedUrls.find((entry) => !entry.includes('localhost')) || advertisedUrls[0];
}
setAdvertisedPort(port);
const logFile = options.logFile || path.join(os.tmpdir(), BRIDGE_LOG_FILE_NAME);
function log(level, message, error) {
const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'info';
const tag = `[wand-remote-bridge] ${message}`;
try { console[method](tag, error || ''); } catch { /* renderer may close console */ }
try {
const detail = error ? ` :: ${error && error.stack ? error.stack : String(error)}` : '';
fs.appendFileSync(logFile, `[${new Date().toISOString()}] [${level}] ${message}${detail}\n`);
} catch { /* best-effort */ }
}
log('info', `Bridge starting (pid=${process.pid}, panelRoot=${panelRoot}, preferredPort=${port}, host=${host})`);
globalThis.__wandRemoteBridgeLogFile = logFile;
function broadcast(type, payload, requestId = null) {
for (const client of clients) {
sendJson(client, type, payload, requestId);
}
}
function sendSnapshot(client) {
if (!currentSnapshot) {
sendJson(client, 'trainer_changed', {
previousTrainerId: null,
trainerId: '',
});
return;
}
sendJson(client, 'trainer_meta', currentSnapshot.trainerMeta);
sendJson(client, 'trainer_values', currentSnapshot.trainerValues);
}
function sync(rawSnapshot) {
const nextSnapshot = rawSnapshot ? normalizeSnapshot(rawSnapshot) : null;
const previousTrainerId = currentSnapshot?.trainerMeta?.trainer?.trainerId ?? null;
const nextTrainerId = nextSnapshot?.trainerMeta?.trainer?.trainerId ?? null;
currentSnapshot = nextSnapshot;
if (previousTrainerId !== nextTrainerId) {
broadcast('trainer_changed', {
previousTrainerId,
trainerId: nextTrainerId || '',
});
}
if (currentSnapshot) {
broadcast('trainer_meta', currentSnapshot.trainerMeta);
broadcast('trainer_values', currentSnapshot.trainerValues);
}
}
function valueChanged(change) {
if (!currentSnapshot || !isRecord(change)) {
return;
}
const target = safeString(change.target);
if (!target) {
return;
}
currentSnapshot.trainerValues.values[target] = cloneValue(change.value);
broadcast('value_changed', {
trainerId: safeString(change.trainerId, currentSnapshot.trainerMeta.trainer.trainerId),
target,
value: cloneValue(change.value),
oldValue: cloneValue(change.oldValue),
source: safeString(change.source, 'desktop'),
cheatId: typeof change.cheatId === 'string' ? change.cheatId : undefined,
});
}
function setHandler(handler) {
setValueHandler = typeof handler === 'function' ? handler : null;
}
function serveFile(response, filePath) {
try {
const content = fs.readFileSync(filePath);
response.writeHead(200, {
'Content-Type': contentTypeFor(filePath),
'Cache-Control': 'no-store',
});
response.end(content);
} catch {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
}
}
const server = http.createServer((request, response) => {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
if (url.pathname === '/' || url.pathname === '') {
response.writeHead(302, { Location: '/remote/' });
response.end();
return;
}
if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) {
response.writeHead(302, { Location: REMOTE_BASE_PATH });
response.end();
return;
}
if (url.pathname === REMOTE_BASE_PATH) {
serveFile(response, path.join(panelRoot, 'index.html'));
return;
}
if (url.pathname === REMOTE_HEALTH_PATH) {
response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({
ok: listening,
trainerId: currentSnapshot?.trainerMeta?.trainer?.trainerId || null,
remoteUrl: globalThis.__wandRemoteBridgeUrl,
advertisedUrls,
}));
return;
}
if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) {
serveFile(response, path.join(panelRoot, url.pathname.replace(REMOTE_BASE_PATH, '')));
return;
}
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
});
server.on('upgrade', (request, socket) => {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
if (url.pathname !== REMOTE_WS_PATH) {
socket.destroy();
return;
}
const key = request.headers['sec-websocket-key'];
if (typeof key !== 'string' || !key) {
socket.destroy();
return;
}
const accept = crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
socket.write([
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${accept}`,
'',
'',
].join('\r\n'));
const client = {
socket,
buffer: Buffer.alloc(0),
closed: false,
};
clients.add(client);
socket.on('data', async (chunk) => {
try {
client.buffer = Buffer.concat([client.buffer, chunk]);
while (client.buffer.length > 0) {
const frame = parseFrame(client.buffer);
if (!frame) {
return;
}
client.buffer = client.buffer.subarray(frame.bytesConsumed);
if (!frame.fin) {
closeClient(client, 1003, 'Fragmented frames are not supported.');
return;
}
if (frame.opcode === 8) {
closeClient(client, 1000, 'Closing');
return;
}
if (frame.opcode === 9) {
client.socket.write(makeFrame(10, frame.payload));
continue;
}
if (frame.opcode !== 1) {
continue;
}
const message = JSON.parse(frame.payload.toString('utf8'));
if (message?.type === 'hello') {
sendJson(client, 'hello_ack', {
sessionId: `sess_${Date.now()}`,
accepted: true,
serverVersion: '0.2.0-wand',
protocolVersion: 1,
remoteUrl: globalThis.__wandRemoteBridgeUrl,
advertisedUrls,
}, message.requestId ?? null);
sendSnapshot(client);
continue;
}
if (message?.type === 'set_value') {
const target = safeString(message.payload?.target);
if (!currentSnapshot || !target || !(target in currentSnapshot.trainerValues.values)) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot?.trainerMeta?.trainer?.trainerId || '',
target,
error: {
code: 'invalid_target',
message: 'Unknown cheat target.',
},
}, message.requestId ?? null);
continue;
}
if (!setValueHandler) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
error: {
code: 'bridge_not_ready',
message: 'The local bridge is not ready to write trainer values yet.',
},
}, message.requestId ?? null);
continue;
}
let result = false;
try {
result = await Promise.resolve(setValueHandler({
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
value: cloneValue(message.payload?.value),
cheatId: typeof message.payload?.cheatId === 'string' ? message.payload.cheatId : undefined,
}));
} catch (error) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
error: {
code: 'set_failed',
message: error instanceof Error ? error.message : 'Failed to set trainer value.',
},
}, message.requestId ?? null);
continue;
}
if (!result) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
error: {
code: 'set_rejected',
message: 'The trainer rejected the requested value.',
},
}, message.requestId ?? null);
continue;
}
sendJson(client, 'set_value_result', {
ok: true,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
}, message.requestId ?? null);
}
}
} catch (error) {
sendJson(client, 'error', {
code: 'invalid_message',
message: error instanceof Error ? error.message : 'Failed to process client message.',
});
}
});
socket.on('close', () => {
client.closed = true;
clients.delete(client);
});
socket.on('end', () => {
client.closed = true;
clients.delete(client);
});
socket.on('error', (error) => {
client.closed = true;
clients.delete(client);
log('warn', 'WebSocket client error.', error);
});
});
server.on('error', (error) => {
if (!listening && error && error.code === 'EADDRINUSE' && port < maxPort) {
const nextPort = port + 1;
log('warn', `Port ${port} is busy, trying ${nextPort}.`);
listen(nextPort);
return;
}
log('warn', `Bridge server error on ${host}:${port}.`, error);
});
server.on('listening', () => {
listening = true;
log('info', `Listening on ${globalThis.__wandRemoteBridgeUrl}`);
});
function listen(nextPort) {
setAdvertisedPort(nextPort);
server.listen(port, host);
}
listen(port);
return {
get listening() {
return listening;
},
get remoteUrl() {
return globalThis.__wandRemoteBridgeUrl;
},
get advertisedUrls() {
return advertisedUrls.slice();
},
sync,
valueChanged,
setHandler,
close() {
for (const client of clients) {
closeClient(client);
}
clients.clear();
currentSnapshot = null;
listening = false;
server.close();
},
};
}
function ensureBridge(options = {}) {
if (!globalThis.__wandRemoteBridgeRuntime) {
globalThis.__wandRemoteBridgeRuntime = createBridgeRuntime(options);
}
return globalThis.__wandRemoteBridgeRuntime;
}
function writeInstallLog(level, message, error) {
const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'info';
const tag = `[wand-remote-bridge] ${message}`;
try { console[method](tag, error || ''); } catch { /* best-effort */ }
try {
const detail = error ? ` :: ${error && error.stack ? error.stack : String(error)}` : '';
fs.appendFileSync(path.join(os.tmpdir(), BRIDGE_LOG_FILE_NAME), `[${new Date().toISOString()}] [${level}] ${message}${detail}\n`);
} catch { /* best-effort */ }
}
function loadRendererScripts(panelRoot, scriptsRoot) {
const root = scriptsRoot || path.join(panelRoot, RENDERER_SCRIPTS_DIR);
if (!fs.existsSync(root)) {
return [];
}
return fs.readdirSync(root)
.filter((name) => name.endsWith('.js'))
.sort((left, right) => left.localeCompare(right))
.map((name) => ({
name,
source: fs.readFileSync(path.join(root, name), 'utf8'),
}));
}
function buildRendererBootstrap(remoteUrl, scripts) {
// Inline each script source directly instead of wrapping it in `new Function(...)`.
// Wand's renderer ships with a strict CSP (no `unsafe-eval`), so any attempt to eval
// a string at runtime — including the `Function` constructor — silently throws
// "EvalError: Refused to evaluate a string as JavaScript". `executeJavaScript`
// itself runs in the page's V8 context and is not affected by CSP, so concatenating
// sources into a single payload makes scripts behave the same as a manual paste in
// DevTools (which is the only path the user reported as working).
const header = `
globalThis.__wandRemoteBridgeUrl = ${JSON.stringify(remoteUrl)};
if (!globalThis.WandEnhancer) {
globalThis.WandEnhancer = Object.freeze({
apiVersion: ${RENDERER_SCRIPT_API_VERSION},
remoteUrl: ${JSON.stringify(remoteUrl)},
log: function () { try { console.info.apply(console, ["[wand-enhancer-script]"].concat(Array.from(arguments))); } catch (_) {} },
});
} else {
try { globalThis.__wandRemoteBridgeUrl = ${JSON.stringify(remoteUrl)}; } catch (_) {}
}
console.info("[wand-remote-bridge] Renderer bootstrap (" + ${scripts.length} + " script(s)).");
`;
const body = scripts.map((script) => {
const tag = JSON.stringify(`wand-enhancer-script-${script.name}`);
return `
;(function (WandEnhancer) {
try {
${script.source}
} catch (error) {
try { console.warn("[wand-remote-bridge] Renderer script failed", ${JSON.stringify(script.name)}, error); } catch (_) {}
}
})(globalThis.WandEnhancer);
//# sourceURL=${tag.slice(1, -1)}
`;
}).join('\n');
return `(() => {\n${header}\n${body}\n})();`;
}
function installRendererScripts(electron, runtime, options = {}) {
if (globalThis.__wandRemoteBridgeRendererScriptsInstalled) {
return;
}
globalThis.__wandRemoteBridgeRendererScriptsInstalled = true;
const scripts = loadRendererScripts(options.panelRoot || __dirname, options.scriptsRoot);
if (scripts.length === 0) {
writeInstallLog('info', 'No renderer scripts found.');
return;
}
electron.app.on('web-contents-created', (_event, contents) => {
const inject = () => {
if (!contents || contents.isDestroyed()) {
return;
}
contents.executeJavaScript(buildRendererBootstrap(runtime.remoteUrl, scripts), true)
.catch((error) => writeInstallLog('warn', 'Failed to inject renderer scripts.', error));
};
contents.on('dom-ready', inject);
contents.on('did-finish-load', inject);
setTimeout(inject, 500);
setTimeout(inject, 2000);
});
writeInstallLog('info', `Renderer script injection installed (${scripts.map((script) => script.name).join(', ')}).`);
}
function installWandRuntime(electron, options = {}) {
const runtime = ensureBridge(options);
if (!electron || !electron.ipcMain || !electron.app) {
throw new Error('Electron main-process API is required to install Wand runtime hooks.');
}
const boundRenderers = globalThis.__wandRemoteBridgeBoundRenderers || new Set();
globalThis.__wandRemoteBridgeBoundRenderers = boundRenderers;
runtime.setHandler((request) => {
let delivered = false;
for (const sender of Array.from(boundRenderers)) {
try {
if (!sender || sender.isDestroyed()) {
boundRenderers.delete(sender);
continue;
}
sender.send('wand-remote-set-value', request);
delivered = true;
} catch (error) {
boundRenderers.delete(sender);
writeInstallLog('warn', 'Failed to forward set_value to renderer.', error);
}
}
return delivered;
});
if (!globalThis.__wandRemoteBridgeIpcInstalled) {
globalThis.__wandRemoteBridgeIpcInstalled = true;
electron.ipcMain.handle('wand-remote-sync', (_event, snapshot) => {
runtime.sync(snapshot);
return true;
});
electron.ipcMain.handle('wand-remote-value-changed', (_event, change) => {
runtime.valueChanged(change);
return true;
});
electron.ipcMain.handle('wand-remote-set-handler-bind', (event) => {
if (event && event.sender) {
boundRenderers.add(event.sender);
}
return true;
});
electron.ipcMain.handle('wand-remote-url', () => runtime.remoteUrl);
}
installRendererScripts(electron, runtime, options);
writeInstallLog('info', 'Wand runtime hooks installed.');
return runtime;
}
module.exports = {
createBridgeRuntime,
ensureBridge,
installWandRuntime,
};
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-mira",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "mist",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "tabler",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+23
View File
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
+140
View File
@@ -0,0 +1,140 @@
{
"trainerMeta": {
"session": {
"instanceId": "mock-instance"
},
"trainer": {
"trainerId": "mock-trainer-1",
"gameId": "mock-game-1",
"displayName": "Mock Adventure",
"titleId": "mock-title-1",
"gameVersion": "1.0.0",
"trainerLoading": false,
"gameInstalled": true,
"needsCompatibilityWarning": false,
"language": "en-US",
"themeId": "default",
"isTimeLimitExpired": false,
"notesReadHash": null
},
"schema": {
"categories": ["player", "inventory", "world"],
"cheats": [
{
"uuid": "toggle-god-mode",
"target": "god_mode",
"type": "toggle",
"name": "God Mode",
"description": "Ignore incoming damage.",
"instructions": null,
"category": "player",
"parent": null,
"args": {}
},
{
"uuid": "slider-health",
"target": "player_health",
"type": "slider",
"name": "Health",
"description": "Tune player health in real time.",
"instructions": null,
"category": "player",
"parent": null,
"args": {
"min": 0,
"max": 100,
"step": 1
}
},
{
"uuid": "number-money",
"target": "player_money",
"type": "number",
"name": "Money",
"description": "Set the current money amount.",
"instructions": null,
"category": "inventory",
"parent": null,
"args": {
"min": 0,
"max": 999999,
"step": 100
}
},
{
"uuid": "button-restock",
"target": "restock_ammo",
"type": "button",
"name": "Restock Ammo",
"description": "Apply a one-shot action.",
"instructions": "Click once to refill ammo.",
"category": "inventory",
"parent": null,
"args": {}
},
{
"uuid": "selection-difficulty",
"target": "difficulty",
"type": "selection",
"name": "Difficulty",
"description": "Pick one predefined option.",
"instructions": null,
"category": "world",
"parent": null,
"args": {
"options": [
{ "label": "Easy", "value": "easy" },
{ "label": "Normal", "value": "normal" },
{ "label": "Hard", "value": "hard" }
]
}
},
{
"uuid": "scalar-speed",
"target": "game_speed",
"type": "scalar",
"name": "Game Speed",
"description": "Scalar-style preset selector.",
"instructions": null,
"category": "world",
"parent": null,
"args": {
"postfix": "x",
"default": 1,
"options": [0.5, 1, 1.5, 2]
}
},
{
"uuid": "incremental-time",
"target": "time_of_day",
"type": "incremental",
"name": "Time of Day",
"description": "Step through a small sequence of values.",
"instructions": null,
"category": "world",
"parent": null,
"args": {
"options": [
{ "label": "Dawn", "value": "dawn" },
{ "label": "Day", "value": "day" },
{ "label": "Dusk", "value": "dusk" },
{ "label": "Night", "value": "night" }
]
}
}
]
}
},
"trainerValues": {
"trainerId": "mock-trainer-1",
"values": {
"god_mode": false,
"player_health": 83,
"player_money": 15000,
"restock_ammo": 0,
"difficulty": "normal",
"game_speed": 1,
"time_of_day": "day"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wand</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "wand-web-panel",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:host": "vite --host 0.0.0.0",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"preview:host": "vite preview --host 0.0.0.0",
"bridge": "node ./bridge/server.mjs"
},
"dependencies": {
"preact": "^10.27.2",
"ws": "^8.18.3"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/vite": "^4.2.1",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^16.5.0",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^7.3.2"
}
}
+2251
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
# Custom Renderer Scripts
Place user scripts here as plain `.js` files before running the Wand patch.
During patching, Wand Enhancer copies default scripts from `web-panel/scripts/default` and user scripts from this folder into `remote-panel/renderer-scripts` inside `app.asar`. Scripts are loaded in filename order on every Wand renderer startup.
Each script runs in the Wand renderer and receives a small global API:
```js
(function (WandEnhancer) {
WandEnhancer.log('custom script loaded', WandEnhancer.remoteUrl);
})(globalThis.WandEnhancer);
```
Use unique global guards for repeat-safe scripts because the renderer can be reinjected after navigation.
@@ -0,0 +1,8 @@
(function installUserHudMarker(WandEnhancer) {
if (globalThis.__wandEnhancerUserHudMarkerInstalled) {
return;
}
globalThis.__wandEnhancerUserHudMarkerInstalled = true;
WandEnhancer.log('user script loaded', WandEnhancer.remoteUrl);
})(globalThis.WandEnhancer);
@@ -0,0 +1,8 @@
(() => {
if (document.documentElement.dataset.wandEnhancerCustomScript === 'loaded') {
return;
}
document.documentElement.dataset.wandEnhancerCustomScript = 'loaded';
console.info('[Wand Enhancer] Custom renderer script loaded');
})();
@@ -0,0 +1,94 @@
(function installRemotePopupCleanup(WandEnhancer) {
if (globalThis.__wandRemotePopupCleanupInstalled) {
return;
}
globalThis.__wandRemotePopupCleanupInstalled = true;
const style = document.createElement('style');
style.id = 'wand-remote-popup-cleanup-style';
style.textContent = `
remote-tooltip .remote-tooltip .top-wrapper,
remote-tooltip .remote-tooltip .remote-tooltip-section-divider,
remote-tooltip .remote-tooltip .instructions .header,
remote-tooltip .remote-tooltip .instructions .content .text,
remote-tooltip .remote-tooltip .instructions .platforms {
display: none !important;
}
remote-tooltip .remote-tooltip .instructions-wrapper {
margin: 0 !important;
padding: 18px !important;
text-align: center !important;
}
remote-tooltip .remote-tooltip .instructions,
remote-tooltip .remote-tooltip .instructions .content {
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
gap: 0 !important;
}
remote-tooltip .remote-tooltip .instructions remote-qr-code {
all: unset !important;
--wand-qr-size: clamp(220px, 100vw, 300px);
width: var(--wand-qr-size) !important;
height: var(--wand-qr-size) !important;
min-width: var(--wand-qr-size) !important;
min-height: var(--wand-qr-size) !important;
max-width: var(--wand-qr-size) !important;
max-height: var(--wand-qr-size) !important;
flex: 0 0 var(--wand-qr-size) !important;
aspect-ratio: 1 / 1 !important;
display: block !important;
border-radius: 12px !important;
overflow: hidden !important;
transform: none !important;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.35) !important;
}
remote-tooltip .remote-tooltip .instructions remote-qr-code canvas {
width: 100% !important;
height: 100% !important;
aspect-ratio: 1 / 1 !important;
display: block !important;
object-fit: contain !important;
image-rendering: pixelated !important;
border-radius: 12px !important;
transform: none !important;
}
`;
const installStyle = () => {
if (!document.getElementById(style.id)) {
document.head.appendChild(style);
}
};
const updateLinks = () => {
const remoteUrl = globalThis.__wandRemoteBridgeUrl || WandEnhancer?.remoteUrl;
if (!remoteUrl) {
return;
}
for (const anchor of document.querySelectorAll('remote-tooltip a[href]')) {
anchor.setAttribute('href', remoteUrl);
anchor.textContent = remoteUrl.replace(/\/$/, '');
}
};
installStyle();
updateLinks();
const observer = new MutationObserver(() => {
installStyle();
updateLinks();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
})(globalThis.WandEnhancer);
+221
View File
@@ -0,0 +1,221 @@
import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { CategorySection } from '@/features/remote-panel/components/CategorySection';
import { ConnectionPanel } from '@/features/remote-panel/components/ConnectionPanel';
import { DeckHeader } from '@/features/remote-panel/components/DeckHeader';
import { EmptyDeck } from '@/features/remote-panel/components/EmptyDeck';
import { TrainerOverview } from '@/features/remote-panel/components/TrainerOverview';
import { buildPinnedGroup, filterGroups, groupCheatsByCategory } from '@/features/remote-panel/category';
import { handleProtocolMessage } from '@/features/remote-panel/message-handler';
import { normalizeOutgoingValue, type CheatSchema, type TrainerMetaPayload } from '@/features/remote-panel/protocol';
import {
getPinnedStorageKey,
loadPinnedTargets,
savePinnedTargets,
} from '@/features/remote-panel/pinned-storage';
import { PanelSocketClient } from '@/features/remote-panel/socket-client';
import { createInitialPanelState, panelReducer } from '@/features/remote-panel/state';
import { Icon } from '@/components/ui/icon';
import { Input } from '@/components/ui/input';
export function App() {
const [state, dispatch] = useReducer(panelReducer, createInitialPanelState());
const [searchQuery, setSearchQuery] = useState('');
const clientRef = useRef<PanelSocketClient | null>(null);
const trainerMetaRef = useRef<TrainerMetaPayload | null>(state.trainerMeta);
useEffect(() => {
return () => {
clientRef.current?.disconnect();
clientRef.current = null;
};
}, []);
useEffect(() => {
trainerMetaRef.current = state.trainerMeta;
}, [state.trainerMeta]);
const groups = useMemo(() => groupCheatsByCategory(state.trainerMeta), [state.trainerMeta]);
const pinnedGroup = useMemo(
() => buildPinnedGroup(state.trainerMeta, state.pinnedTargets),
[state.trainerMeta, state.pinnedTargets],
);
const filteredGroups = useMemo(() => filterGroups(groups, searchQuery), [groups, searchQuery]);
const filteredPinnedGroup = useMemo(
() => (pinnedGroup ? filterGroups([pinnedGroup], searchQuery)[0] ?? null : null),
[pinnedGroup, searchQuery],
);
const pinnedStorageKey = useMemo(
() => getPinnedStorageKey(state.trainerMeta?.trainer ?? null),
[state.trainerMeta?.trainer],
);
const activeTrainer = state.trainerMeta?.trainer ?? null;
const cheatCount = state.trainerMeta?.schema.cheats.length ?? 0;
const controlsDisabled = Boolean(activeTrainer?.trainerLoading || activeTrainer?.isTimeLimitExpired);
useEffect(() => {
dispatch({ type: 'setPinnedTargets', pinned: loadPinnedTargets(pinnedStorageKey) });
}, [pinnedStorageKey]);
function connect(): void {
clientRef.current?.disconnect();
const wsUrl = state.wsUrl.trim();
if (!wsUrl) {
dispatch({ type: 'error', message: 'Enter a WebSocket URL first.' });
return;
}
const nextClient = new PanelSocketClient(wsUrl, {
onConnecting: () => dispatch({ type: 'connecting' }),
onOpen: () => dispatch({ type: 'connected' }),
onMessage: (message) => handleProtocolMessage(dispatch, message, trainerMetaRef.current),
onClose: () => dispatch({ type: 'error', message: 'The WebSocket connection closed.' }),
onError: (message) => dispatch({ type: 'error', message }),
});
clientRef.current = nextClient;
nextClient.connect();
}
function handleCheatChange(cheat: CheatSchema, nextValue: unknown): void {
const normalizedValue = normalizeOutgoingValue(cheat, nextValue);
dispatch({ type: 'setPending', target: cheat.target, pending: true });
dispatch({ type: 'valueChanged', target: cheat.target, value: normalizedValue });
if (state.connectionStatus !== 'connected' || !state.trainerMeta || !clientRef.current) {
dispatch({ type: 'setPending', target: cheat.target, pending: false });
return;
}
const sent = clientRef.current.setValue(state.trainerMeta.trainer.trainerId, cheat.target, normalizedValue, cheat.uuid);
if (!sent) {
dispatch({ type: 'setPending', target: cheat.target, pending: false });
dispatch({ type: 'error', message: 'The bridge socket is not open.' });
}
}
function handleTogglePin(cheat: CheatSchema): void {
const next = { ...state.pinnedTargets };
if (next[cheat.target]) {
delete next[cheat.target];
} else {
next[cheat.target] = true;
}
dispatch({ type: 'togglePinnedTarget', target: cheat.target });
savePinnedTargets(pinnedStorageKey, next);
}
async function loadDebugSession(): Promise<void> {
if (!import.meta.env.DEV) {
return;
}
clientRef.current?.disconnect();
const debugSession = await import('@/features/remote-panel/debug-session');
debugSession.loadDebugSession(dispatch);
}
useEffect(() => {
if (import.meta.env.DEV) {
void import('@/features/remote-panel/debug-session').then((debugSession) => {
if (debugSession.isDebugSessionRequested()) {
debugSession.loadDebugSession(dispatch);
return;
}
if (state.wsUrl.trim()) {
connect();
}
});
return;
}
if (!state.wsUrl.trim()) {
return;
}
connect();
}, []);
return (
<main className="min-h-svh overflow-hidden bg-background px-2 py-2 text-foreground sm:px-5 sm:py-3 lg:px-8">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-3 sm:gap-4">
<DeckHeader connectionStatus={state.connectionStatus} remoteUrl={state.remoteUrl} />
<div className="grid gap-3 sm:gap-4 xl:grid-cols-[360px_minmax(0,1fr)]">
<aside className="space-y-3 sm:space-y-4">
<ConnectionPanel
status={state.connectionStatus}
wsUrl={state.wsUrl}
lastError={state.lastError}
onConnect={connect}
onDebugSession={import.meta.env.DEV ? loadDebugSession : undefined}
onWsUrlChange={(wsUrl) => dispatch({ type: 'setWsUrl', wsUrl })}
/>
</aside>
<section className="space-y-4 sm:space-y-5">
{activeTrainer ? (
<>
<TrainerOverview trainer={activeTrainer} cheatCount={cheatCount} categoryCount={groups.length} />
<div className="relative">
<Icon className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" name="search" />
<Input
type="text"
value={searchQuery}
onInput={(event) => setSearchQuery((event.target as HTMLInputElement).value)}
placeholder="Search cheats, categories, targets..."
className="h-9 pl-8 pr-8 text-sm"
/>
{searchQuery ? (
<button
type="button"
onClick={() => setSearchQuery('')}
aria-label="Clear search"
className="absolute right-2 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:text-white"
>
<Icon className="size-3.5" name="x" />
</button>
) : null}
</div>
<div className="space-y-4 sm:space-y-7">
{filteredPinnedGroup ? (
<CategorySection
key="__pinned__"
group={filteredPinnedGroup}
values={state.values}
pendingTargets={state.pendingTargets}
pinnedTargets={state.pinnedTargets}
disabled={controlsDisabled}
onCheatChange={handleCheatChange}
onTogglePin={handleTogglePin}
/>
) : null}
{filteredGroups.map((group) => (
<CategorySection
key={group.id}
group={group}
values={state.values}
pendingTargets={state.pendingTargets}
pinnedTargets={state.pinnedTargets}
disabled={controlsDisabled}
onCheatChange={handleCheatChange}
onTogglePin={handleTogglePin}
/>
))}
{searchQuery && filteredGroups.length === 0 && !filteredPinnedGroup ? (
<p className="rounded-[8px] border border-white/10 bg-white/4.5 px-3 py-4 text-center text-sm text-muted-foreground">
No cheats match "{searchQuery}".
</p>
) : null}
</div>
</>
) : (
<EmptyDeck />
)}
</section>
</div>
</div>
</main>
);
}
+29
View File
@@ -0,0 +1,29 @@
import type { ComponentProps } from "react"
import { cn } from "@/lib/utils"
type BadgeVariant = "default" | "outline"
const BADGE_BASE = "inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium whitespace-nowrap"
const BADGE_VARIANTS: Record<BadgeVariant, string> = {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline: "border-border bg-input/20 text-foreground",
}
function Badge({
className,
variant = "default",
...props
}: ComponentProps<"span"> & { variant?: BadgeVariant }) {
return (
<span
data-slot="badge"
data-variant={variant}
className={cn(BADGE_BASE, BADGE_VARIANTS[variant], className)}
{...props}
/>
)
}
export { Badge }
+36
View File
@@ -0,0 +1,36 @@
import type { ComponentProps } from "react"
import { cn } from "@/lib/utils"
type ButtonVariant = "default" | "outline"
type ButtonSize = "default" | "icon"
const BUTTON_BASE = "inline-flex shrink-0 items-center justify-center rounded-md border border-transparent text-xs font-medium whitespace-nowrap transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50"
const BUTTON_VARIANTS: Record<ButtonVariant, string> = {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline: "border-border hover:bg-input/50 hover:text-foreground",
}
const BUTTON_SIZES: Record<ButtonSize, string> = {
default: "h-7 gap-1 px-2",
icon: "size-7",
}
function Button({
className,
variant = "default",
size = "default",
...props
}: ComponentProps<"button"> & { variant?: ButtonVariant; size?: ButtonSize }) {
return (
<button
type="button"
data-slot="button"
className={cn(BUTTON_BASE, BUTTON_VARIANTS[variant], BUTTON_SIZES[size], className)}
{...props}
/>
)
}
export { Button }
+70
View File
@@ -0,0 +1,70 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-xs text-card-foreground ring-1 ring-foreground/10",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"grid auto-rows-min items-start gap-1 rounded-t-lg px-4",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("font-heading text-sm font-medium", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-xs/relaxed text-muted-foreground", className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
}

Some files were not shown because too many files have changed in this diff Show More