mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-29 22:01:18 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7f0eae670 | |||
| 1c9a8fb780 | |||
| ec07dc63f0 | |||
| 88556ec70f | |||
| c02bad919d | |||
| b9faf80f86 | |||
| 6395ca3a27 | |||
| 4ce47dc6d2 | |||
| 8c6d87671c | |||
| a0b3968d33 | |||
| 6906a67a2e | |||
| 37ce6b3f4a | |||
| 8756e41fb9 | |||
| 544b9f0fb0 | |||
| a4f3a57f97 | |||
| 1f5ba9fc95 | |||
| 710d014e6d | |||
| a810e5b549 | |||
| 13759b1db6 | |||
| 3b2f373946 | |||
| f78f9609b5 | |||
| 633984ed3b | |||
| fa753b4a34 | |||
| 5273930eef | |||
| 9a5d3799bf | |||
| d02b84a414 | |||
| a943b91f5f | |||
| a5583fd4f7 | |||
| 13e26c3a70 | |||
| d8d0abd448 | |||
| 54fe538ed9 | |||
| 8f14ee2939 | |||
| 9ba60a09a7 | |||
| 108feeafa8 | |||
| b283c63fd7 |
@@ -0,0 +1,4 @@
|
||||
# The web panel is the bundled frontend shipped inside the C# patcher.
|
||||
# Mark it as vendored so GitHub Linguist keeps it out of the repository's
|
||||
# language statistics — the project is a C# app, not a TypeScript one.
|
||||
web-panel/** linguist-vendored
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repo_root=$(dirname "$script_dir")
|
||||
script_path="$repo_root/scripts/validate-release-metadata.ps1"
|
||||
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
script_path=$(cygpath -w "$script_path")
|
||||
fi
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$script_path"
|
||||
+8
-1
@@ -1 +1,8 @@
|
||||
ko_fi: kitbyte
|
||||
patreon: kitbyte
|
||||
|
||||
custom: [
|
||||
"https://www.patreon.com/kitbyte/gift",
|
||||
"https://tronscan.org/#/address/TQdvau8pAy5Tg1Aa588tTcPCFgbcHtuoxc",
|
||||
"https://www.blockchain.com/explorer/addresses/btc/1EZKDcyU8REm9JW5xwXJqSpn5Xaq5yAWWX",
|
||||
"https://etherscan.io/address/0xd904d9d0557f88bbb1c4ab3582b4ca0d8a730e8d"
|
||||
]
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
if: github.repository == 'k1tbyte/Wand-Enhancer'
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
RELEASE_VERSION: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: web-panel/pnpm-lock.yaml
|
||||
|
||||
- name: Validate release metadata
|
||||
shell: pwsh
|
||||
run: ./scripts/validate-release-metadata.ps1 -ExpectedVersion $env:RELEASE_VERSION
|
||||
|
||||
- name: Extract release notes from changelog
|
||||
shell: pwsh
|
||||
run: ./scripts/get-changelog-section.ps1 -Version $env:RELEASE_VERSION -OutputPath release-notes.md
|
||||
|
||||
- name: Build release
|
||||
shell: pwsh
|
||||
run: ./build.ps1 -Configuration Release
|
||||
|
||||
- name: Publish GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ github.ref_name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
body_path: release-notes.md
|
||||
files: |
|
||||
WandEnhancer/bin/Release/WandEnhancer.exe
|
||||
CHANGELOG.md
|
||||
fail_on_unmatched_files: true
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Validate Release Metadata
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "main" ]
|
||||
pull_request:
|
||||
branches: [ "master", "main" ]
|
||||
|
||||
jobs:
|
||||
validate-release-metadata:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate version and changelog sync
|
||||
shell: pwsh
|
||||
run: ./scripts/validate-release-metadata.ps1
|
||||
+14
-3
@@ -130,8 +130,19 @@ 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
|
||||
.tmp
|
||||
.source
|
||||
web-panel/bridge/wand-remote-bridge.cjs
|
||||
@@ -0,0 +1,46 @@
|
||||
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.
|
||||
- The Electron bridge is authored as modular CommonJS source under `web-panel/bridge/source.cjs` and `web-panel/bridge/bridge-modules/`, but production runtime must be bundled/minified into `web-panel/dist/bridge.cjs` by `pnpm run build:bridge`. Do not copy `bridge-modules` into Wand or embed them as ASAR resources.
|
||||
- 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 script sources live in `web-panel/bridge/scripts/default/` and are bundled/minified into `web-panel/dist/renderer-scripts/` by `pnpm run build:bridge`. 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.
|
||||
- `web-panel/bridge/scripts/default/installed-apps-sync.js` resolves Wand's renderer services/store and publishes `My Games` snapshots through the `wand-remote-installed-apps` IPC channel. The synced list must mirror Wand's `my_games` source criteria: catalog games come from `installedGameVersions`, and extra installed unsupported titles come from `correlatedUnavailableTitles` whose `games[].correlationIds` match `installedApps`.
|
||||
- If the injected renderer cannot read a populated `correlatedUnavailableTitles` slice from the live store, `installed-apps-sync.js` must fall back to Wand's `/v3/unavailable_titles` correlation lookup through the renderer API client instead of degrading to raw install entries or an empty `My Games` list.
|
||||
- Installed app snapshots should include game artwork in `imageUrl` when possible. `installed-apps-sync.js` must prefer Wand's own client icon CDN shape `https://api-cdn.wemod.com/steam_community/<steamAppId>/client_icon/96.webp` whenever the matched title/game/version metadata contains a Steam AppID, regardless of install platform. Do not assume `steamAppId` is a flat property; search nested `steam*` metadata before falling back to installed Steam `sku`. If metadata still does not expose the icon, fall back to the rendered Wand sidebar DOM (`.sidebar-game-row-image` background-image) keyed by `titleId` parsed from `data-tooltip-trigger-for`. The web panel `GameCover` must tolerate broken artwork URLs and fall back to its text cover.
|
||||
- The same renderer sync script also forwards lifecycle state through `wand-remote-game-status`: `game-launched` / `game-ended` come from Wand's launch monitor service, and trainer runtime comes from the running-trainer visibility service. The web panel consumes this as the `game_status` websocket message.
|
||||
- When Wand does not emit a `game-launched` event but a trainer is already active, `wand-remote-game-status` must synthesize a running session from the running-trainer visibility payload so the remote panel does not show an idle game session next to a running trainer.
|
||||
- The websocket `hello` snapshot must still send cached `installed_apps` and `game_status` even when no trainer snapshot is active yet; do not reintroduce a handshake path that returns early after `trainer_changed`.
|
||||
- Remote Play/Stop uses the websocket `remote_command` message. The bridge forwards it over `wand-remote-command` / `wand-remote-command-response`, and `installed-apps-sync.js` resolves Wand's trainer API + trainer service to launch a trainer for a `gameId` or end the current trainer.
|
||||
- Remote Play must construct Wand's real trainer launch request class (`69482.vO`) before calling `trainerService.launch(...)`. Passing a plain object launches the game process but breaks Wand's `getMetadata(vO)`-based trainer state, causing missing status, disappearing play/close buttons, and stuck loading behavior.
|
||||
- Pro activation is a C# asar patch (`EPatchType.ActivatePro`, independent of the remote panel / bridge). It rewrites three account-returning service methods to inject `subscription:{period:"yearly",state:"active"}` into the response before it reaches the store: `getUserAccount` and `setAccountWandBrandExperience` (Resolver-style, service field via `<service_name>` placeholder) and `setAccountLanguage` (`BuildSetAccountLanguagePatch` PatchFactory — captures the real param names + the original `post("/v3/account/language",{...})` expr and wraps `.then`). A fourth patch (`setAccountReducer`) rewrites the `ACTION_SET_ACCOUNT` store reducer so any account write (periodic `refreshAccount`, push/profile updates, etc.) keeps Pro even when it bypasses those API methods. Pro is `am(account) = !!account.subscription` (flags/512 are irrelevant). `setAccountLanguage` is the one the original two patches missed, which is why Pro dropped on language change. If a future Wand build changes these method bodies, re-derive the regexes against the live `app-*.bundle.js` (do NOT trust `.source/new` — it is a different version).
|
||||
|
||||
## ASAR Patch Pipeline
|
||||
|
||||
- Preserve and restore both `resources/app.asar` and `resources/app.asar.unpacked` backups.
|
||||
- Inject `web-panel/dist` as `remote-panel/`; it must already contain `bridge.cjs` and generated default renderer scripts under `renderer-scripts/`. Selected/local custom renderer scripts are then copied under `remote-panel/renderer-scripts`.
|
||||
- Do not commit extracted `.source/` or `.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.
|
||||
- Custom quick presets are per trainer/game and stored by `preset-storage.ts` under `localStorage` key `wand-remote.presets.v1:<gameId-or-trainerId>`. Presets capture persistent cheat values only; do not include `button` one-shot cheats in saved presets.
|
||||
- All `localStorage` access in `web-panel/src/features/remote-panel/` MUST go through the shared helpers in `storage.ts` (`loadJson` / `saveJson` / `loadStringSet` / `saveStringSet`). Do not reintroduce per-module `try/catch` + `JSON.parse` duplication in `pinned-storage`, `preset-storage`, or `game-pin-storage`. Trainer/game storage IDs are derived through the shared `getTrainerStorageId(trainer)` helper in `storage.ts`; do not re-implement the `gameId → titleId → trainerId → 'global'` precedence inline.
|
||||
- All shared bridge port/path/IPC channel/WS-opcode/protocol-version constants live in `web-panel/bridge/bridge-modules/constants.cjs` (exports `IPC_CHANNEL`, `WS_OPCODE`, `BRIDGE_PROTOCOL_VERSION`, `BRIDGE_SERVER_VERSION`, `RENDERER_INJECTION_DELAYS_MS`). Do not redeclare `3223`, `/remote/*`, IPC channel strings, raw WS opcode numbers (1/8/9/10), or the 500/2000 ms injection delays inline. The renderer-script-side equivalents (e.g. `vO`/`TRAINER_LAUNCH_REQUEST_EXPORT_KEY`, snapshot key prefixes, bootstrap log throttle) live in `web-panel/bridge/scripts/default/installed-apps-sync/constants.js`.
|
||||
- UI string-union types follow the `E*` enum convention from `.claude/rules/frontend-conventions.md` (currently `ECheatType` in `protocol.ts`, `EConnectionStatus` in `state.ts`); the wire string values must remain on the right-hand side of the enum members. Reducer `PanelAction` `type` tags stay as discriminated-union string literals (the union itself provides the discrimination — converting it to an enum loses pattern matching).
|
||||
- Cheat input controls live one-per-file under `web-panel/src/features/remote-panel/controls/` (`ToggleControl`, `SliderControl`, `ScalarControl`, `NumberControl`, `ActionButton`, `SelectionControl`, `IncrementalControl`); shared `SliderTrack` / `StepButton` / `ControlInternalProps` are in `controls/shared.tsx` and number formatting helpers in `controls/format-number.ts`. `controls/CheatControl.tsx` is a thin dispatcher map keyed by `ECheatType` — do not inline new control bodies into it.
|
||||
- Mobile drawer performance is sensitive to `backdrop-filter`. Keep drawer panels and nested glass controls blur-free under coarse pointers, and do not add per-row `backdrop-blur-*` inside drawer lists.
|
||||
|
||||
## Validation
|
||||
|
||||
- Web panel build: `cd web-panel && pnpm run build` (runs type-check, Vite build, then `build:bridge` into `dist`).
|
||||
- Bridge/script syntax checks after build: `node --check web-panel/dist/bridge.cjs` and `node --check web-panel/dist/renderer-scripts/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`.
|
||||
+25
-25
@@ -1,14 +1,13 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AsarSharp.AsarFileSystem;
|
||||
using AsarSharp.Integrity;
|
||||
using AsarSharp.Utils;
|
||||
|
||||
namespace AsarSharp
|
||||
{
|
||||
|
||||
public class CreateOptions
|
||||
{
|
||||
public Regex Unpack { get; set; }
|
||||
@@ -28,41 +27,36 @@ namespace AsarSharp
|
||||
_destPath = destPath ?? throw new ArgumentNullException(nameof(destPath));
|
||||
_options = options;
|
||||
}
|
||||
|
||||
|
||||
public void CreatePackageWithOptions()
|
||||
{
|
||||
var result = FileSystemCrawler.CrawlFileSystem(_folderPath);
|
||||
var result = FileSystemCrawler.CrawlFileSystem(_folderPath);
|
||||
_filenames = result.filenames;
|
||||
_metadata = result.metadata;
|
||||
CreatePackageFromFiles();
|
||||
}
|
||||
|
||||
|
||||
public void CreatePackageFromFiles()
|
||||
{
|
||||
var filesystem = new Filesystem(_folderPath);
|
||||
var files = new List<Disk.BasicFileInfo>();
|
||||
|
||||
var filenamesSorted = _filenames.ToList();
|
||||
|
||||
foreach (var filename in filenamesSorted)
|
||||
var files = new List<Disk.BasicFileInfo>(_filenames.Count);
|
||||
|
||||
foreach (var filename in _filenames)
|
||||
{
|
||||
HandleFile(filesystem, filename, files);
|
||||
}
|
||||
|
||||
InsertsDone(filesystem, files);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void HandleFile(Filesystem filesystem, string filename, List<Disk.BasicFileInfo> files)
|
||||
{
|
||||
if (!_metadata.ContainsKey(filename))
|
||||
if (!_metadata.TryGetValue(filename, out var file))
|
||||
{
|
||||
var fileType = FileSystemCrawler.DetermineFileType(filename);
|
||||
_metadata[filename] = fileType ?? throw new Exception("Unknown file type for file: " + filename);
|
||||
file = FileSystemCrawler.DetermineFileType(filename)
|
||||
?? throw new Exception("Unknown file type for file: " + filename);
|
||||
_metadata[filename] = file;
|
||||
}
|
||||
var file = _metadata[filename];
|
||||
|
||||
switch (file.Type)
|
||||
{
|
||||
@@ -70,9 +64,13 @@ namespace AsarSharp
|
||||
filesystem.InsertDirectory(filename, false);
|
||||
break;
|
||||
case FileType.File:
|
||||
var shouldUnpack = ShouldUnpackPath(Extensions.GetRelativePath(_folderPath, Path.GetDirectoryName(filename)));
|
||||
string parentDir = Path.GetDirectoryName(filename) ?? string.Empty;
|
||||
string relParent = Extensions.GetRelativePath(_folderPath, parentDir);
|
||||
bool shouldUnpack = ShouldUnpackPath(relParent);
|
||||
long fileSize = file.Stat is FileInfo fi ? fi.Length : 0;
|
||||
var placeholder = IntegrityHelper.CreatePlaceholder(fileSize);
|
||||
files.Add(new Disk.BasicFileInfo { Filename = filename, Unpack = shouldUnpack });
|
||||
filesystem.InsertFile(filename, shouldUnpack, file);
|
||||
filesystem.InsertFile(filename, shouldUnpack, file, placeholder);
|
||||
break;
|
||||
case FileType.Link:
|
||||
throw new NotImplementedException();
|
||||
@@ -81,14 +79,16 @@ namespace AsarSharp
|
||||
|
||||
private bool ShouldUnpackPath(string relativePath)
|
||||
{
|
||||
return _options.Unpack?.IsMatch(relativePath) == true;
|
||||
return _options?.Unpack?.IsMatch(relativePath) == true;
|
||||
}
|
||||
|
||||
private void InsertsDone(Filesystem filesystem, List<Disk.BasicFileInfo> files)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_destPath) ?? throw new InvalidOperationException());
|
||||
Disk.WriteFileSystem(_destPath, filesystem, new Disk.FilesystemFilesAndLinks { Files = files, Links = null }, _metadata);
|
||||
Directory.CreateDirectory(
|
||||
Path.GetDirectoryName(_destPath)
|
||||
?? throw new InvalidOperationException());
|
||||
Disk.WriteFileSystem(_destPath, filesystem,
|
||||
new Disk.FilesystemFilesAndLinks { Files = files, Links = null }, _metadata);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+137
-74
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -10,98 +10,63 @@ namespace AsarSharp
|
||||
{
|
||||
public class AsarExtractor
|
||||
{
|
||||
private const int IO_BUFFER_SIZE = 1024 * 1024;
|
||||
private const int FS_INTERNAL_BUFFER = 4096;
|
||||
|
||||
public static void ExtractAll(string archivePath, string dest)
|
||||
{
|
||||
var filesystem = Disk.ReadFilesystemSync(archivePath);
|
||||
var filenames = filesystem.ListFiles();
|
||||
|
||||
// under windows just extract links as regular files
|
||||
// On Windows, links are extracted as plain files.
|
||||
bool followLinks = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
// create destination directory
|
||||
Directory.CreateDirectory(dest);
|
||||
|
||||
byte[] ioBuffer = new byte[IO_BUFFER_SIZE];
|
||||
var dirCache = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFullPath(dest) };
|
||||
var extractionErrors = new List<Exception>();
|
||||
foreach (var fullPath in filenames)
|
||||
string rootPath = filesystem.GetRootPath();
|
||||
long dataOffset = 8 + filesystem.GetHeaderSize();
|
||||
|
||||
// One archive handle for all reads — old code opened it per file.
|
||||
using (var archive = new FileStream(rootPath, FileMode.Open, FileAccess.Read, FileShare.Read,
|
||||
FS_INTERNAL_BUFFER, FileOptions.RandomAccess))
|
||||
{
|
||||
try
|
||||
foreach (var fullPath in filenames)
|
||||
{
|
||||
// Remove leading slash
|
||||
var filename = fullPath.Substring(1);
|
||||
var destFilename = Path.Combine(dest, filename);
|
||||
var file = filesystem.GetFile(filename, followLinks);
|
||||
|
||||
// Check that the file is not written outside the specified destination folder
|
||||
string relativePath = Extensions.GetRelativePath(dest, destFilename);
|
||||
if (relativePath.StartsWith(".."))
|
||||
try
|
||||
{
|
||||
throw new InvalidOperationException($"{fullPath}: file \"{destFilename}\" writes out of the package");
|
||||
}
|
||||
var filename = fullPath.Substring(1);
|
||||
var destFilename = Path.Combine(dest, filename);
|
||||
var file = filesystem.GetFile(filename, followLinks);
|
||||
|
||||
if (file.IsDirectory)
|
||||
{
|
||||
// it's a directory, create it and continue with the next entry
|
||||
Directory.CreateDirectory(destFilename);
|
||||
}
|
||||
// TODO (LINK NOT SUPPORTED)
|
||||
else if (file.IsLink)
|
||||
{
|
||||
// it's a symlink, create a symlink
|
||||
var linkSrcPath = Extensions.GetDirectoryName(Path.Combine(dest, file.Link));
|
||||
var linkDestPath = Extensions.GetDirectoryName(destFilename);
|
||||
var relativeLinkPath = Extensions.GetRelativePath(linkDestPath, linkSrcPath);
|
||||
|
||||
// try to delete output file, because we can't overwrite a link
|
||||
try
|
||||
{
|
||||
File.Delete(destFilename);
|
||||
}
|
||||
catch {
|
||||
// Ignore errors during file link deletion
|
||||
}
|
||||
|
||||
var linkTo = Path.Combine(relativeLinkPath, Path.GetFileName(file.Link));
|
||||
|
||||
if (Extensions.GetRelativePath(dest, linkSrcPath).StartsWith(".."))
|
||||
// Path-traversal guard.
|
||||
string relativePath = Extensions.GetRelativePath(dest, destFilename);
|
||||
if (relativePath.StartsWith(".."))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{fullPath}: file \"{file.Link}\" links out of the package to \"{linkSrcPath}\"");
|
||||
$"{fullPath}: file \"{destFilename}\" writes out of the package");
|
||||
}
|
||||
|
||||
// On Windows, creating symlinks requires additional permissions or enabling Developer Mode,
|
||||
// so just copy the contents of the file
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
if (file.IsDirectory)
|
||||
{
|
||||
var targetPath = Path.Combine(linkSrcPath, Path.GetFileName(file.Link));
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
Directory.CreateDirectory(destFilename);
|
||||
Extensions.CopyDirectory(targetPath, destFilename);
|
||||
}
|
||||
else if (File.Exists(targetPath))
|
||||
{
|
||||
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
|
||||
File.Copy(targetPath, destFilename, true);
|
||||
}
|
||||
EnsureDirectory(destFilename, dirCache);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
|
||||
if (file.IsLink)
|
||||
{
|
||||
// On Unix systems we use symlinks
|
||||
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
|
||||
Extensions.CreateSymbolicLink(linkTo, destFilename);
|
||||
ExtractLink(dest, fullPath, destFilename, file, dirCache);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (file.IsFile)
|
||||
{
|
||||
// it's a file, try to extract it
|
||||
|
||||
if (!file.IsFile) continue;
|
||||
|
||||
try
|
||||
{
|
||||
byte[] content;
|
||||
|
||||
content = Disk.ReadFileSync(filesystem, filename, file);
|
||||
|
||||
File.WriteAllBytes(destFilename, content);
|
||||
|
||||
ExtractFile(archive, dataOffset, rootPath, filename, destFilename, file, ioBuffer, dirCache);
|
||||
|
||||
if (file.Executable == true && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Extensions.SetUnixFilePermission(destFilename, "755");
|
||||
@@ -112,10 +77,10 @@ namespace AsarSharp
|
||||
extractionErrors.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
extractionErrors.Add(ex);
|
||||
catch (Exception ex)
|
||||
{
|
||||
extractionErrors.Add(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,5 +92,103 @@ namespace AsarSharp
|
||||
extractionErrors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureDirectory(string path, HashSet<string> cache)
|
||||
{
|
||||
string full = Path.GetFullPath(path);
|
||||
if (cache.Contains(full)) return;
|
||||
Directory.CreateDirectory(full);
|
||||
// Mark every ancestor too so siblings skip the syscall.
|
||||
string p = full;
|
||||
while (!string.IsNullOrEmpty(p) && cache.Add(p))
|
||||
{
|
||||
p = Path.GetDirectoryName(p);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureParentDir(string filePath, HashSet<string> cache)
|
||||
{
|
||||
string parent = Path.GetDirectoryName(filePath);
|
||||
if (string.IsNullOrEmpty(parent)) return;
|
||||
EnsureDirectory(parent, cache);
|
||||
}
|
||||
|
||||
private static void ExtractFile(FileStream archive, long dataOffset, string rootPath,
|
||||
string filename, string destFilename, FilesystemEntry file, byte[] buffer,
|
||||
HashSet<string> dirCache)
|
||||
{
|
||||
EnsureParentDir(destFilename, dirCache);
|
||||
|
||||
if (file.Unpacked == true)
|
||||
{
|
||||
string unpackedSourcePath = Path.GetFullPath(Path.Combine($"{rootPath}.unpacked", filename));
|
||||
string unpackedDestPath = Path.GetFullPath(destFilename);
|
||||
|
||||
if (string.Equals(unpackedSourcePath, unpackedDestPath, StringComparison.OrdinalIgnoreCase))
|
||||
return; // self-copy
|
||||
if (!File.Exists(unpackedSourcePath))
|
||||
return; // header references a missing unpacked file — skip rather than abort
|
||||
|
||||
File.Copy(unpackedSourcePath, destFilename, true);
|
||||
return;
|
||||
}
|
||||
|
||||
long size = file.Size ?? 0;
|
||||
using (var dst = new FileStream(destFilename, FileMode.Create, FileAccess.Write, FileShare.None,
|
||||
FS_INTERNAL_BUFFER, FileOptions.SequentialScan))
|
||||
{
|
||||
if (size <= 0) return;
|
||||
|
||||
archive.Position = dataOffset + long.Parse(file.Offset);
|
||||
long remaining = size;
|
||||
while (remaining > 0)
|
||||
{
|
||||
int toRead = remaining > buffer.Length ? buffer.Length : (int)remaining;
|
||||
int got = archive.Read(buffer, 0, toRead);
|
||||
if (got <= 0) throw new EndOfStreamException("Archive truncated");
|
||||
dst.Write(buffer, 0, got);
|
||||
remaining -= got;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractLink(string dest, string fullPath, string destFilename,
|
||||
FilesystemEntry file, HashSet<string> dirCache)
|
||||
{
|
||||
var linkSrcPath = Extensions.GetDirectoryName(Path.Combine(dest, file.Link));
|
||||
var linkDestPath = Extensions.GetDirectoryName(destFilename);
|
||||
var relativeLinkPath = Extensions.GetRelativePath(linkDestPath, linkSrcPath);
|
||||
|
||||
try { File.Delete(destFilename); }
|
||||
catch { /* ignore — failing to remove an existing link is non-fatal */ }
|
||||
|
||||
var linkTo = Path.Combine(relativeLinkPath, Path.GetFileName(file.Link));
|
||||
|
||||
if (Extensions.GetRelativePath(dest, linkSrcPath).StartsWith(".."))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{fullPath}: file \"{file.Link}\" links out of the package to \"{linkSrcPath}\"");
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var targetPath = Path.Combine(linkSrcPath, Path.GetFileName(file.Link));
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
EnsureDirectory(destFilename, dirCache);
|
||||
Extensions.CopyDirectory(targetPath, destFilename);
|
||||
}
|
||||
else if (File.Exists(targetPath))
|
||||
{
|
||||
EnsureParentDir(destFilename, dirCache);
|
||||
File.Copy(targetPath, destFilename, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureParentDir(destFilename, dirCache);
|
||||
Extensions.CreateSymbolicLink(linkTo, destFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AsarSharp.Integrity;
|
||||
using AsarSharp.PickleTools;
|
||||
using AsarSharp.Utils;
|
||||
using Newtonsoft.Json;
|
||||
@@ -9,7 +11,9 @@ namespace AsarSharp.AsarFileSystem
|
||||
{
|
||||
public static class Disk
|
||||
{
|
||||
private static Dictionary<string, Filesystem> _filesystemCache = new Dictionary<string, Filesystem>();
|
||||
private const int StreamBufferSize = 1024 * 1024;
|
||||
private static readonly ConcurrentDictionary<string, Filesystem> _filesystemCache =
|
||||
new ConcurrentDictionary<string, Filesystem>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public class ArchiveHeader
|
||||
{
|
||||
@@ -29,36 +33,29 @@ namespace AsarSharp.AsarFileSystem
|
||||
public string Filename { get; set; }
|
||||
public bool Unpack { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region Reading
|
||||
|
||||
|
||||
public static ArchiveHeader ReadArchiveHeaderSync(string archivePath)
|
||||
{
|
||||
using (FileStream fs = File.OpenRead(archivePath))
|
||||
{
|
||||
// read the size of the header (8 bytes)
|
||||
using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read, FileShare.Read,
|
||||
65536, FileOptions.SequentialScan))
|
||||
{
|
||||
byte[] sizeBuf = new byte[8];
|
||||
if (fs.Read(sizeBuf, 0, 8) != 8)
|
||||
{
|
||||
throw new Exception("Unable to read header size");
|
||||
}
|
||||
|
||||
|
||||
var sizePickle = Pickle.CreateFromBuffer(sizeBuf);
|
||||
var size = sizePickle.CreateIterator().ReadUInt32();
|
||||
|
||||
// Read the header of the specified size
|
||||
|
||||
var headerBuf = new byte[size];
|
||||
if(fs.Read(headerBuf, 0, (int)size) != size)
|
||||
{
|
||||
if (fs.Read(headerBuf, 0, (int)size) != size)
|
||||
throw new Exception("Unable to read header");
|
||||
}
|
||||
|
||||
|
||||
var headerPickle = Pickle.CreateFromBuffer(headerBuf);
|
||||
var header = headerPickle.CreateIterator().ReadString();
|
||||
|
||||
var headerObj = JsonConvert.DeserializeObject<FilesystemEntry>(header);
|
||||
|
||||
|
||||
return new ArchiveHeader
|
||||
{
|
||||
Header = headerObj,
|
||||
@@ -67,137 +64,161 @@ namespace AsarSharp.AsarFileSystem
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static Filesystem ReadFilesystemSync(string archivePath)
|
||||
{
|
||||
if (!_filesystemCache.ContainsKey(archivePath) || _filesystemCache[archivePath] == null)
|
||||
return _filesystemCache.GetOrAdd(archivePath, key =>
|
||||
{
|
||||
ArchiveHeader header = ReadArchiveHeaderSync(archivePath);
|
||||
Filesystem filesystem = new Filesystem(archivePath);
|
||||
var header = ReadArchiveHeaderSync(key);
|
||||
var filesystem = new Filesystem(key);
|
||||
filesystem.SetHeader(header.Header, header.HeaderSize);
|
||||
_filesystemCache[archivePath] = filesystem;
|
||||
}
|
||||
|
||||
return _filesystemCache[archivePath];
|
||||
return filesystem;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static byte[] ReadFileSync(Filesystem filesystem, string filename, FilesystemEntry info)
|
||||
{
|
||||
if (!info.IsFile || !info.Size.HasValue)
|
||||
{
|
||||
throw new ArgumentException("Entry is not a file", nameof(info));
|
||||
}
|
||||
|
||||
long size = info.Size.Value;
|
||||
byte[] buffer = new byte[size];
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
if (size <= 0) return buffer;
|
||||
|
||||
if (info.Unpacked == true)
|
||||
{
|
||||
// It's an unpacked file, read it directly
|
||||
string filePath = Path.Combine($"{filesystem.GetRootPath()}.unpacked", filename);
|
||||
return File.ReadAllBytes(filePath);
|
||||
}
|
||||
|
||||
// Read from the ASAR archive
|
||||
using (FileStream fs = File.OpenRead(filesystem.GetRootPath()))
|
||||
using (var fs = new FileStream(filesystem.GetRootPath(), FileMode.Open, FileAccess.Read,
|
||||
FileShare.Read, 65536, FileOptions.RandomAccess))
|
||||
{
|
||||
// Important: the offset must take into account the size of the Pickle header (8 bytes)
|
||||
// and the size of the header itself
|
||||
long offset = 8 + filesystem.GetHeaderSize() + long.Parse(info.Offset);
|
||||
fs.Position = offset;
|
||||
|
||||
// Read the whole file at once
|
||||
int bytesRead = fs.Read(buffer, 0, (int)size);
|
||||
if (bytesRead != size)
|
||||
{
|
||||
throw new Exception($"Failed to read entire file, got {bytesRead} bytes instead of {size}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
public static bool UncacheFilesystem(string archivePath)
|
||||
{
|
||||
if (_filesystemCache.ContainsKey(archivePath))
|
||||
{
|
||||
_filesystemCache.Remove(archivePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return _filesystemCache.TryRemove(archivePath, out _);
|
||||
}
|
||||
|
||||
public static void UncacheAll()
|
||||
{
|
||||
_filesystemCache.Clear();
|
||||
}
|
||||
|
||||
|
||||
public static void CopyFile(string dest, string rootPath, string filename)
|
||||
{
|
||||
if(dest == null || rootPath == null || filename == null)
|
||||
if (dest == null || rootPath == null || filename == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
if (dest == rootPath)
|
||||
{
|
||||
string normalizedDestRoot = Path.GetFullPath(dest)
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
string normalizedRootPath = Path.GetFullPath(rootPath)
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
|
||||
if (string.Equals(normalizedDestRoot, normalizedRootPath, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
string sourcePath = Path.GetFullPath(Path.Combine(rootPath, filename));
|
||||
string destPath = Path.GetFullPath(Path.Combine(dest, filename));
|
||||
|
||||
if (string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
|
||||
string sourcePath = Path.Combine(rootPath, filename);
|
||||
string destPath = Path.Combine(dest, filename);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destPath) ?? throw new InvalidOperationException());
|
||||
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
|
||||
using (var destinationStream = new FileStream(destPath, FileMode.Create, FileAccess.Write))
|
||||
using (var src = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamBufferSize, FileOptions.SequentialScan))
|
||||
using (var dst = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None, StreamBufferSize, FileOptions.SequentialScan))
|
||||
{
|
||||
sourceStream.CopyTo(destinationStream);
|
||||
src.CopyTo(dst, StreamBufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void WriteFileSystem(string dest, Filesystem fileSystem,
|
||||
FilesystemFilesAndLinks lists,
|
||||
Dictionary<string, CrawledFileType> metadata)
|
||||
FilesystemFilesAndLinks lists, Dictionary<string, CrawledFileType> metadata)
|
||||
{
|
||||
var fsHeader = fileSystem.GetHeader();
|
||||
var headerPickle = Pickle.CreateEmpty();
|
||||
var serializerSettings = new JsonSerializerSettings()
|
||||
{ NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore } ;
|
||||
|
||||
var headerJson = JsonConvert.SerializeObject(fsHeader,serializerSettings);
|
||||
headerPickle.WriteString(headerJson);
|
||||
var headerBuf = headerPickle.ToBuffer();
|
||||
|
||||
var sizePickle = Pickle.CreateEmpty();
|
||||
sizePickle.WriteUInt32((uint)headerBuf.Length);
|
||||
var sizeBuf = sizePickle.ToBuffer();
|
||||
|
||||
using (FileStream fs = File.Create(dest))
|
||||
var serializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
fs.Write(sizeBuf, 0, sizeBuf.Length);
|
||||
fs.Write(headerBuf, 0, headerBuf.Length);
|
||||
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore
|
||||
};
|
||||
|
||||
// --- Phase 1: write placeholder header ---
|
||||
string headerJson = JsonConvert.SerializeObject(fileSystem.GetHeader(), serializerSettings);
|
||||
var headerPickle = Pickle.CreateEmpty();
|
||||
headerPickle.WriteString(headerJson);
|
||||
|
||||
var sizePickle = Pickle.CreateEmpty();
|
||||
sizePickle.WriteUInt32((uint)headerPickle.GetTotalSize());
|
||||
int sizePickleSize = sizePickle.GetTotalSize();
|
||||
|
||||
var buf = new byte[StreamBufferSize];
|
||||
var blockBuf = new byte[4 * 1024 * 1024]; // shared across all files — avoids 4MB alloc per file
|
||||
|
||||
using (var fs = new FileStream(dest, FileMode.Create, FileAccess.Write, FileShare.None, StreamBufferSize, FileOptions.SequentialScan))
|
||||
{
|
||||
sizePickle.WriteTo(fs);
|
||||
headerPickle.WriteTo(fs);
|
||||
|
||||
// --- Phase 2: stream files, hash in one pass, patch nodes in-memory ---
|
||||
foreach (var file in lists.Files)
|
||||
{
|
||||
if (file.Unpack)
|
||||
{
|
||||
var filename = Extensions.GetRelativePath(fileSystem.GetRootPath(), file.Filename);
|
||||
CopyFile($"{dest}.unpacked", fileSystem.GetRootPath(), filename);
|
||||
var relName = Extensions.GetRelativePath(fileSystem.GetRootPath(), file.Filename);
|
||||
CopyFile($"{dest}.unpacked", fileSystem.GetRootPath(), relName);
|
||||
CopyAndHash(file.Filename, null, buf, blockBuf, fileSystem);
|
||||
continue;
|
||||
}
|
||||
using (var transformedFileStream = new FileStream(file.Filename, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
transformedFileStream.CopyTo(fs);
|
||||
}
|
||||
|
||||
CopyAndHash(file.Filename, fs, buf, blockBuf, fileSystem);
|
||||
}
|
||||
|
||||
// --- Phase 3: re-serialize header with real hashes, seek back, overwrite ---
|
||||
string patchedJson = JsonConvert.SerializeObject(fileSystem.GetHeader(), serializerSettings);
|
||||
var patchedPickle = Pickle.CreateEmpty();
|
||||
patchedPickle.WriteString(patchedJson);
|
||||
|
||||
var patchedSizePickle = Pickle.CreateEmpty();
|
||||
patchedSizePickle.WriteUInt32((uint)patchedPickle.GetTotalSize());
|
||||
|
||||
fs.Position = 0;
|
||||
patchedSizePickle.WriteTo(fs);
|
||||
patchedPickle.WriteTo(fs);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyAndHash(string srcPath, Stream dest, byte[] buf, byte[] blockBuf, Filesystem fs)
|
||||
{
|
||||
string relPath = Extensions.GetRelativePath(fs.GetRootPath(), srcPath);
|
||||
var node = fs.GetNode(relPath, followLinks: false);
|
||||
|
||||
long fileSize = node?.Size ?? 0;
|
||||
int estimatedBlocks = fileSize > 0 ? (int)((fileSize + 4 * 1024 * 1024 - 1) / (4 * 1024 * 1024)) : 0;
|
||||
|
||||
using (var hasher = new IntegrityHelper.StreamingHasher(estimatedBlocks, blockBuf))
|
||||
using (var src = new FileStream(srcPath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamBufferSize, FileOptions.SequentialScan))
|
||||
{
|
||||
int read;
|
||||
while ((read = src.Read(buf, 0, buf.Length)) > 0)
|
||||
{
|
||||
hasher.Append(buf, 0, read);
|
||||
dest?.Write(buf, 0, read);
|
||||
}
|
||||
|
||||
if (node != null)
|
||||
node.Integrity = hasher.Finalise();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AsarSharp.Integrity;
|
||||
@@ -13,7 +13,7 @@ namespace AsarSharp.AsarFileSystem
|
||||
private int _headerSize;
|
||||
private long _offset;
|
||||
|
||||
private const uint UINT32_MAX = 0xFFFFFFFF; // 2^32 - 1
|
||||
private const uint UINT32_MAX = 0xFFFFFFFF;
|
||||
|
||||
public Filesystem(string src)
|
||||
{
|
||||
@@ -23,20 +23,9 @@ namespace AsarSharp.AsarFileSystem
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
public string GetRootPath()
|
||||
{
|
||||
return _src;
|
||||
}
|
||||
|
||||
public FilesystemEntry GetHeader()
|
||||
{
|
||||
return _header;
|
||||
}
|
||||
|
||||
public int GetHeaderSize()
|
||||
{
|
||||
return _headerSize;
|
||||
}
|
||||
public string GetRootPath() => _src;
|
||||
public FilesystemEntry GetHeader() => _header;
|
||||
public int GetHeaderSize() => _headerSize;
|
||||
|
||||
public void SetHeader(FilesystemEntry header, int headerSize)
|
||||
{
|
||||
@@ -47,82 +36,94 @@ namespace AsarSharp.AsarFileSystem
|
||||
public FilesystemEntry SearchNodeFromDirectory(string p)
|
||||
{
|
||||
FilesystemEntry json = _header;
|
||||
|
||||
// Normalize path delimiters to system delimiters
|
||||
p = p.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
|
||||
|
||||
string[] dirs = p.Split(Path.DirectorySeparatorChar);
|
||||
|
||||
foreach (string dir in dirs)
|
||||
|
||||
int len = p.Length;
|
||||
int start = 0;
|
||||
|
||||
// skip leading separators
|
||||
while (start < len && (p[start] == '/' || p[start] == '\\')) start++;
|
||||
|
||||
while (start < len)
|
||||
{
|
||||
if (dir == "." || string.IsNullOrEmpty(dir)) continue;
|
||||
|
||||
if (json.IsDirectory)
|
||||
// find next separator
|
||||
int end = start;
|
||||
while (end < len && p[end] != '/' && p[end] != '\\') end++;
|
||||
|
||||
int segLen = end - start;
|
||||
if (segLen == 0 || (segLen == 1 && p[start] == '.'))
|
||||
{
|
||||
if (!json.Files.ContainsKey(dir))
|
||||
{
|
||||
json.Files[dir] = new FilesystemEntry { Files = new Dictionary<string, FilesystemEntry>(StringComparer.Ordinal) };
|
||||
}
|
||||
json = json.Files[dir];
|
||||
start = end + 1;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
string seg = p.Substring(start, segLen);
|
||||
|
||||
if (!json.IsDirectory)
|
||||
throw new Exception($"Unexpected directory state while traversing: {p}");
|
||||
|
||||
if (!json.Files.TryGetValue(seg, out var child))
|
||||
{
|
||||
child = new FilesystemEntry { Files = new Dictionary<string, FilesystemEntry>(StringComparer.Ordinal) };
|
||||
json.Files[seg] = child;
|
||||
}
|
||||
json = child;
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
public (FilesystemEntry parent, string name) SearchNodeFromPathWithParent(string p)
|
||||
{
|
||||
string rel = Extensions.GetRelativePath(_src, p);
|
||||
if (string.IsNullOrEmpty(rel))
|
||||
return (_header, string.Empty);
|
||||
|
||||
string name = Path.GetFileName(rel);
|
||||
string dir = Extensions.GetDirectoryName(rel);
|
||||
var parent = SearchNodeFromDirectory(dir);
|
||||
|
||||
if (parent.Files == null)
|
||||
parent.Files = new Dictionary<string, FilesystemEntry>(StringComparer.Ordinal);
|
||||
|
||||
if (!parent.Files.ContainsKey(name))
|
||||
parent.Files[name] = new FilesystemEntry();
|
||||
|
||||
return (parent, name);
|
||||
}
|
||||
|
||||
public List<string> ListFiles(bool isPack = false)
|
||||
{
|
||||
var files = new List<string>();
|
||||
|
||||
FillFilesFromMetadata("/", _header);
|
||||
return files;
|
||||
|
||||
void FillFilesFromMetadata(string basePath, FilesystemEntry metadata)
|
||||
{
|
||||
if (!metadata.IsDirectory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!metadata.IsDirectory) return;
|
||||
foreach (var entry in metadata.Files)
|
||||
{
|
||||
string childPath = entry.Key;
|
||||
FilesystemEntry childMetadata = entry.Value;
|
||||
string fullPath = Path.Combine(basePath, childPath).Replace('\\', '/');
|
||||
|
||||
string packState =
|
||||
childMetadata.Unpacked == true ? "unpack" : "pack ";
|
||||
|
||||
string fullPath = Path.Combine(basePath, entry.Key).Replace('\\', '/');
|
||||
string packState = entry.Value.Unpacked == true ? "unpack" : "pack ";
|
||||
files.Add(isPack ? $"{packState} : {fullPath}" : fullPath);
|
||||
FillFilesFromMetadata(fullPath, childMetadata);
|
||||
FillFilesFromMetadata(fullPath, entry.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FilesystemEntry GetNode(string p, bool followLinks = true)
|
||||
{
|
||||
// Normalize path delimiters
|
||||
p = p.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
|
||||
|
||||
FilesystemEntry node = SearchNodeFromDirectory(Extensions.GetDirectoryName(p));
|
||||
string name = Path.GetFileName(p);
|
||||
|
||||
// Process symbolic links
|
||||
|
||||
if (node.IsLink && followLinks)
|
||||
{
|
||||
return GetNode(Path.Combine(node.Link, name));
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
if (node.IsDirectory && node.Files.TryGetValue(name, out var entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -132,104 +133,62 @@ namespace AsarSharp.AsarFileSystem
|
||||
public FilesystemEntry GetFile(string p, bool followLinks = true)
|
||||
{
|
||||
FilesystemEntry info = GetNode(p, followLinks);
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
throw new Exception($"\"{p}\" was not found in this archive");
|
||||
}
|
||||
|
||||
// If followLinks=false, do not allow symbolic links (TODO)
|
||||
if (info.IsLink && followLinks)
|
||||
{
|
||||
return GetFile(info.Link, followLinks);
|
||||
}
|
||||
|
||||
if (info == null) throw new Exception($"\"{p}\" was not found in this archive");
|
||||
if (info.IsLink && followLinks) return GetFile(info.Link, followLinks);
|
||||
return info;
|
||||
}
|
||||
|
||||
public static string ReadLink(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return Path.GetFileName(path);
|
||||
// TODO , NOT IMPLEMENTED
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static string ReadLink(string path) => throw new NotImplementedException();
|
||||
|
||||
#region Writing
|
||||
|
||||
|
||||
public FilesystemEntry SearchNodeFromPath(string p)
|
||||
{
|
||||
p = Extensions.GetRelativePath(_src, p);
|
||||
|
||||
if (string.IsNullOrEmpty(p))
|
||||
{
|
||||
return _header;
|
||||
}
|
||||
|
||||
var name = Path.GetFileName(p);
|
||||
var node = SearchNodeFromDirectory(Extensions.GetDirectoryName(p));
|
||||
|
||||
if (node.Files == null)
|
||||
{
|
||||
node.Files = new Dictionary<string, FilesystemEntry>();
|
||||
}
|
||||
|
||||
if (!node.Files.ContainsKey(name))
|
||||
{
|
||||
node.Files[name] = new FilesystemEntry();
|
||||
}
|
||||
|
||||
return node.Files[name];
|
||||
var (parent, name) = SearchNodeFromPathWithParent(p);
|
||||
if (string.IsNullOrEmpty(name)) return _header;
|
||||
return parent.Files[name];
|
||||
}
|
||||
|
||||
|
||||
public void InsertDirectory(string p, bool unpack)
|
||||
{
|
||||
FilesystemEntry node = SearchNodeFromPath(p);
|
||||
node.Files = node.Files ?? new Dictionary<string, FilesystemEntry>();
|
||||
node.Files = node.Files ?? new Dictionary<string, FilesystemEntry>(StringComparer.Ordinal);
|
||||
node.Unpacked = unpack;
|
||||
}
|
||||
|
||||
public void InsertFile(string path, bool shouldUnpack, CrawledFileType file)
|
||||
|
||||
public void InsertFile(string path, bool shouldUnpack, CrawledFileType file,
|
||||
IntegrityHelper.FileIntegrity precomputedIntegrity = null)
|
||||
{
|
||||
var dirName = Path.GetDirectoryName(path);
|
||||
var dirNode = SearchNodeFromPath(dirName);
|
||||
var (dirNode, _) = SearchNodeFromPathWithParent(Path.GetDirectoryName(path) ?? path);
|
||||
var node = SearchNodeFromPath(path);
|
||||
|
||||
long size = 0;
|
||||
if (file.Stat is FileInfo fileInfo)
|
||||
{
|
||||
size = fileInfo.Length;
|
||||
}
|
||||
long size;
|
||||
if (file.Stat is FileInfo fi)
|
||||
size = fi.Length;
|
||||
else
|
||||
{
|
||||
throw new Exception($"{path}: stat is not a file");
|
||||
}
|
||||
|
||||
|
||||
if (shouldUnpack || dirNode.Unpacked == true)
|
||||
{
|
||||
node.Size = size;
|
||||
node.Unpacked = true;
|
||||
node.Integrity = IntegrityHelper.GetFileIntegrity(path);
|
||||
node.Integrity = precomputedIntegrity ?? IntegrityHelper.GetFileIntegrity(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the file size does not exceed UINT32_MAX
|
||||
if (size > UINT32_MAX)
|
||||
{
|
||||
throw new Exception($"{path}: file size cannot be larger than 4.2GB");
|
||||
}
|
||||
|
||||
node.Size = size;
|
||||
node.Offset = _offset.ToString();
|
||||
node.Integrity = IntegrityHelper.GetFileIntegrity(path);
|
||||
node.Integrity = precomputedIntegrity ?? IntegrityHelper.GetFileIntegrity(path);
|
||||
|
||||
if (!Extensions.IsWindowsPlatform() && (file.Stat.Attributes & FileAttributes.Hidden) != 0)
|
||||
{
|
||||
node.Executable = true;
|
||||
}
|
||||
|
||||
_offset += size;
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AsarSharp.Utils;
|
||||
|
||||
namespace AsarSharp.AsarFileSystem
|
||||
@@ -25,136 +24,108 @@ namespace AsarSharp.AsarFileSystem
|
||||
Directory,
|
||||
Link
|
||||
}
|
||||
|
||||
|
||||
public static class FileSystemCrawler
|
||||
{
|
||||
|
||||
|
||||
public static CrawledFileType DetermineFileType(string filename)
|
||||
{
|
||||
var fileInfo = new FileInfo(filename);
|
||||
if (fileInfo.Exists)
|
||||
FileAttributes attributes;
|
||||
try
|
||||
{
|
||||
return new CrawledFileType { Type = FileType.File, Stat = fileInfo };
|
||||
attributes = File.GetAttributes(filename);
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(filename);
|
||||
if (directoryInfo.Exists)
|
||||
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||
{
|
||||
return new CrawledFileType { Type = FileType.Directory, Stat = directoryInfo };
|
||||
return null;
|
||||
}
|
||||
|
||||
var linkInfo = new FileInfo(filename);
|
||||
if (linkInfo.Exists && (linkInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
{
|
||||
return new CrawledFileType { Type = FileType.Link, Stat = linkInfo };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
bool isDirectory = (attributes & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
bool isLink = (attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
|
||||
FileSystemInfo info = isDirectory
|
||||
? (FileSystemInfo)new DirectoryInfo(filename)
|
||||
: new FileInfo(filename);
|
||||
|
||||
if (isLink) return new CrawledFileType { Type = FileType.Link, Stat = info };
|
||||
if (isDirectory) return new CrawledFileType { Type = FileType.Directory, Stat = info };
|
||||
return new CrawledFileType { Type = FileType.File, Stat = info };
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static (List<string> filenames, Dictionary<string, CrawledFileType> metadata) CrawlFileSystem(string dir)
|
||||
{
|
||||
var metadata = new Dictionary<string, CrawledFileType>();
|
||||
var crawled = CrawlIterative(dir);
|
||||
var results = crawled.Select(filename => new { filename, type = DetermineFileType(filename) }).ToList();
|
||||
|
||||
var links = new List<string>();
|
||||
var filenames = new List<string>();
|
||||
var links = new List<string>();
|
||||
|
||||
foreach (var result in results.Where(result => result.type != null))
|
||||
foreach (var fullPath in CrawlIterative(dir))
|
||||
{
|
||||
metadata[result.filename] = result.type;
|
||||
if (result.type.Type == FileType.Link)
|
||||
{
|
||||
links.Add(result.filename);
|
||||
}
|
||||
filenames.Add(result.filename);
|
||||
var type = DetermineFileType(fullPath);
|
||||
if (type == null) continue;
|
||||
metadata[fullPath] = type;
|
||||
if (type.Type == FileType.Link) links.Add(fullPath);
|
||||
filenames.Add(fullPath);
|
||||
}
|
||||
|
||||
var filteredFilenames = new List<string>();
|
||||
if (links.Count == 0) return (filenames, metadata);
|
||||
|
||||
var filtered = new List<string>(filenames.Count);
|
||||
foreach (var filename in filenames)
|
||||
{
|
||||
var exactLinkIndex = links.FindIndex(link => filename == link);
|
||||
var isValid = true;
|
||||
bool isValid = true;
|
||||
string fileDir = Path.GetDirectoryName(filename) ?? string.Empty;
|
||||
|
||||
for (var i = 0; i < links.Count; i++)
|
||||
foreach (var link in links)
|
||||
{
|
||||
if (i == exactLinkIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.Equals(filename, link, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
var link = links[i];
|
||||
var isFileWithinSymlinkDir = filename.StartsWith(link, StringComparison.OrdinalIgnoreCase);
|
||||
var relativePath = Extensions.GetRelativePath(link, Path.GetDirectoryName(filename) ?? string.Empty);
|
||||
|
||||
if (isFileWithinSymlinkDir && !relativePath.StartsWith("..", StringComparison.Ordinal))
|
||||
if (filename.StartsWith(link, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isValid = false;
|
||||
break;
|
||||
string rel = Extensions.GetRelativePath(link, fileDir);
|
||||
if (!rel.StartsWith("..", StringComparison.Ordinal))
|
||||
{
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid)
|
||||
{
|
||||
filteredFilenames.Add(filename);
|
||||
}
|
||||
if (isValid) filtered.Add(filename);
|
||||
}
|
||||
|
||||
return (filteredFilenames, metadata);
|
||||
return (filtered, metadata);
|
||||
}
|
||||
|
||||
|
||||
// (File order is not important!!!)
|
||||
public static List<string> CrawlIterative(string dir)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var stack = new Stack<string>();
|
||||
var stack = new Stack<DirectoryInfo>();
|
||||
|
||||
|
||||
string basePath = Extensions.GetBasePath(dir);
|
||||
if (!Directory.Exists(basePath)) return result;
|
||||
|
||||
if (!Directory.Exists(basePath))
|
||||
return result;
|
||||
|
||||
// Add only the base directory to the stack, but not to the result
|
||||
stack.Push(basePath);
|
||||
stack.Push(new DirectoryInfo(basePath));
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
string currentDir = stack.Pop();
|
||||
|
||||
var current = stack.Pop();
|
||||
FileSystemInfo[] entries;
|
||||
try
|
||||
{
|
||||
// Add all files from the current directory
|
||||
result.AddRange(Directory.GetFiles(currentDir, "*", SearchOption.TopDirectoryOnly));
|
||||
|
||||
// Add subdirectories to the results and to the stack
|
||||
foreach (var directory in Directory.GetDirectories(currentDir, "*",
|
||||
SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
// Add subdirectories to the result
|
||||
if (directory != basePath) // Do not add a base directory
|
||||
{
|
||||
result.Add(directory);
|
||||
}
|
||||
|
||||
// Add to the stack for processing
|
||||
stack.Push(directory);
|
||||
}
|
||||
entries = current.GetFileSystemInfos();
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// Skip directories to which there is no access
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
result.Add(entry.FullName);
|
||||
if (entry is DirectoryInfo subDir)
|
||||
stack.Push(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
@@ -9,61 +9,148 @@ namespace AsarSharp.Integrity
|
||||
public static class IntegrityHelper
|
||||
{
|
||||
private const string ALGORITHM = "SHA256";
|
||||
// 4MB default block size
|
||||
private const int BLOCK_SIZE = 4 * 1024 * 1024;
|
||||
public const string PLACEHOLDER_HASH = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
private static readonly char[] HexDigits = "0123456789abcdef".ToCharArray();
|
||||
|
||||
public class FileIntegrity
|
||||
{
|
||||
[JsonProperty("algorithm")]
|
||||
public string Algorithm { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("hash")]
|
||||
public string Hash { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("blockSize")]
|
||||
public int BlockSize { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("blocks")]
|
||||
public List<string> Blocks { get; set; }
|
||||
}
|
||||
|
||||
public static FileIntegrity GetFileIntegrity(string path)
|
||||
public static FileIntegrity CreatePlaceholder(long fileSize)
|
||||
{
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using(var fileHash = SHA256.Create())
|
||||
{
|
||||
int blockCount = fileSize > 0 ? (int)((fileSize + BLOCK_SIZE - 1) / BLOCK_SIZE) : 0;
|
||||
var blocks = new List<string>(blockCount);
|
||||
for (int i = 0; i < blockCount; i++)
|
||||
blocks.Add(PLACEHOLDER_HASH);
|
||||
|
||||
var blockHashes = new List<string>();
|
||||
var buffer = new byte[BLOCK_SIZE];
|
||||
return new FileIntegrity
|
||||
{
|
||||
Algorithm = ALGORITHM,
|
||||
Hash = PLACEHOLDER_HASH,
|
||||
BlockSize = BLOCK_SIZE,
|
||||
Blocks = blocks,
|
||||
};
|
||||
}
|
||||
|
||||
public static FileIntegrity GetFileIntegrity(string path, byte[] reusableBuffer = null)
|
||||
{
|
||||
bool ownBuffer = reusableBuffer == null;
|
||||
if (ownBuffer) reusableBuffer = new byte[BLOCK_SIZE];
|
||||
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read,
|
||||
65536, FileOptions.SequentialScan))
|
||||
using (var fileHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
|
||||
using (var blockHash = SHA256.Create())
|
||||
{
|
||||
int estimatedBlockCount = fileStream.Length > 0
|
||||
? (int)((fileStream.Length + BLOCK_SIZE - 1) / BLOCK_SIZE)
|
||||
: 0;
|
||||
var blockHashes = new List<string>(estimatedBlockCount);
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = fileStream.Read(buffer, 0, BLOCK_SIZE)) > 0)
|
||||
while ((bytesRead = fileStream.Read(reusableBuffer, 0, reusableBuffer.Length)) > 0)
|
||||
{
|
||||
var block = new byte[bytesRead];
|
||||
Array.Copy(buffer, block, bytesRead);
|
||||
blockHashes.Add(HashBlock(block));
|
||||
fileHash.TransformBlock(block, 0, block.Length, null, 0);
|
||||
blockHashes.Add(ToLowerHex(blockHash.ComputeHash(reusableBuffer, 0, bytesRead)));
|
||||
fileHash.AppendData(reusableBuffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
fileHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
|
||||
return new FileIntegrity
|
||||
{
|
||||
Algorithm = ALGORITHM,
|
||||
Hash = BitConverter.ToString(fileHash.Hash).Replace("-", "").ToLowerInvariant(),
|
||||
Hash = ToLowerHex(fileHash.GetHashAndReset()),
|
||||
BlockSize = BLOCK_SIZE,
|
||||
Blocks = blockHashes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string HashBlock(byte[] block)
|
||||
public sealed class StreamingHasher : IDisposable
|
||||
{
|
||||
using (var sha256 = SHA256.Create())
|
||||
private readonly IncrementalHash _fileHash;
|
||||
private readonly SHA256 _blockHash;
|
||||
private readonly byte[] _blockBuf;
|
||||
private int _blockFill;
|
||||
private readonly List<string> _blockHashes;
|
||||
|
||||
public StreamingHasher(int estimatedBlocks = 0, byte[] sharedBlockBuffer = null)
|
||||
{
|
||||
var hash = sha256.ComputeHash(block);
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
_fileHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
_blockHash = SHA256.Create();
|
||||
_blockBuf = sharedBlockBuffer ?? new byte[BLOCK_SIZE];
|
||||
_blockFill = 0;
|
||||
_blockHashes = new List<string>(estimatedBlocks);
|
||||
}
|
||||
|
||||
public void Append(byte[] data, int offset, int count)
|
||||
{
|
||||
_fileHash.AppendData(data, offset, count);
|
||||
|
||||
int remaining = count;
|
||||
int src = offset;
|
||||
while (remaining > 0)
|
||||
{
|
||||
int space = BLOCK_SIZE - _blockFill;
|
||||
int copy = Math.Min(space, remaining);
|
||||
Buffer.BlockCopy(data, src, _blockBuf, _blockFill, copy);
|
||||
_blockFill += copy;
|
||||
src += copy;
|
||||
remaining -= copy;
|
||||
|
||||
if (_blockFill == BLOCK_SIZE)
|
||||
{
|
||||
_blockHashes.Add(ToLowerHex(_blockHash.ComputeHash(_blockBuf, 0, BLOCK_SIZE)));
|
||||
_blockFill = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FileIntegrity Finalise()
|
||||
{
|
||||
if (_blockFill > 0)
|
||||
{
|
||||
_blockHashes.Add(ToLowerHex(_blockHash.ComputeHash(_blockBuf, 0, _blockFill)));
|
||||
_blockFill = 0;
|
||||
}
|
||||
|
||||
return new FileIntegrity
|
||||
{
|
||||
Algorithm = ALGORITHM,
|
||||
Hash = ToLowerHex(_fileHash.GetHashAndReset()),
|
||||
BlockSize = BLOCK_SIZE,
|
||||
Blocks = _blockHashes,
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_fileHash.Dispose();
|
||||
_blockHash.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static string ToLowerHex(byte[] bytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0) return string.Empty;
|
||||
var chars = new char[bytes.Length * 2];
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
byte v = bytes[i];
|
||||
chars[i * 2] = HexDigits[v >> 4];
|
||||
chars[i * 2 + 1] = HexDigits[v & 0x0F];
|
||||
}
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+93
-219
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace AsarSharp.PickleTools
|
||||
@@ -12,10 +13,10 @@ namespace AsarSharp.PickleTools
|
||||
public const int SIZE_FLOAT = 4;
|
||||
public const int SIZE_DOUBLE = 8;
|
||||
|
||||
// Size of memory allocation unit for payload
|
||||
public const int PAYLOAD_UNIT = 64;
|
||||
// Initial payload allocation. Bumped from 64 — large headers used to
|
||||
// realloc many times when growing geometrically from 64.
|
||||
public const int PAYLOAD_UNIT = 4096;
|
||||
|
||||
// Maximum value for read-only
|
||||
public const long CAPACITY_READ_ONLY = 9007199254740992;
|
||||
|
||||
private byte[] _header;
|
||||
@@ -57,55 +58,40 @@ namespace AsarSharp.PickleTools
|
||||
SetPayloadSize(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static Pickle CreateEmpty()
|
||||
{
|
||||
return new Pickle();
|
||||
}
|
||||
|
||||
public static Pickle CreateFromBuffer(byte[] buffer)
|
||||
{
|
||||
return new Pickle(buffer);
|
||||
}
|
||||
|
||||
public byte[] GetHeader()
|
||||
{
|
||||
return _header;
|
||||
}
|
||||
public static Pickle CreateEmpty() => new Pickle();
|
||||
public static Pickle CreateFromBuffer(byte[] buffer) => new Pickle(buffer);
|
||||
|
||||
public int GetHeaderSize()
|
||||
{
|
||||
return _headerSize;
|
||||
}
|
||||
|
||||
public PickleIterator CreateIterator()
|
||||
{
|
||||
return new PickleIterator(this);
|
||||
}
|
||||
public byte[] GetHeader() => _header;
|
||||
public int GetHeaderSize() => _headerSize;
|
||||
|
||||
/// <summary>
|
||||
/// Converts Pickle to a byte array
|
||||
/// </summary>
|
||||
public PickleIterator CreateIterator() => new PickleIterator(this);
|
||||
|
||||
/// <summary>Total byte length of the serialised pickle (header + payload).</summary>
|
||||
public int GetTotalSize() => _headerSize + GetPayloadSize();
|
||||
|
||||
/// <summary>Materialise the pickle into a fresh byte array (allocates).</summary>
|
||||
public byte[] ToBuffer()
|
||||
{
|
||||
int resultSize = _headerSize + GetPayloadSize();
|
||||
int resultSize = GetTotalSize();
|
||||
byte[] result = new byte[resultSize];
|
||||
Array.Copy(_header, 0, result, 0, resultSize);
|
||||
Buffer.BlockCopy(_header, 0, result, 0, resultSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteBool(bool value)
|
||||
/// <summary>Write the serialised pickle straight to <paramref name="stream"/> — no extra copy.</summary>
|
||||
public void WriteTo(Stream stream)
|
||||
{
|
||||
return WriteInt(value ? 1 : 0);
|
||||
stream.Write(_header, 0, GetTotalSize());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public bool WriteBool(bool value) => WriteInt(value ? 1 : 0);
|
||||
|
||||
public bool WriteInt(int value)
|
||||
{
|
||||
EnsureCapacity(SIZE_INT32);
|
||||
|
||||
var dataLength = AlignInt(SIZE_INT32, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
const int dataLength = SIZE_INT32; // already 4-byte aligned
|
||||
int newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
@@ -113,13 +99,6 @@ namespace AsarSharp.PickleTools
|
||||
}
|
||||
|
||||
WriteInt32LE(value, _headerSize + _writeOffset);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + SIZE_INT32;
|
||||
for (int i = endOffset; i < endOffset + dataLength - SIZE_INT32; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
@@ -128,10 +107,8 @@ namespace AsarSharp.PickleTools
|
||||
|
||||
public bool WriteUInt32(uint value)
|
||||
{
|
||||
EnsureCapacity(SIZE_UINT32);
|
||||
|
||||
var dataLength = AlignInt(SIZE_UINT32, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
const int dataLength = SIZE_UINT32;
|
||||
int newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
@@ -139,24 +116,15 @@ namespace AsarSharp.PickleTools
|
||||
}
|
||||
|
||||
WriteUInt32LE(value, _headerSize + _writeOffset);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + SIZE_UINT32;
|
||||
for (int i = endOffset; i < endOffset + dataLength - SIZE_UINT32; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteInt64(long value)
|
||||
{
|
||||
EnsureCapacity(SIZE_INT64);
|
||||
|
||||
var dataLength = AlignInt(SIZE_INT64, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
const int dataLength = SIZE_INT64;
|
||||
int newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
@@ -164,13 +132,6 @@ namespace AsarSharp.PickleTools
|
||||
}
|
||||
|
||||
WriteInt64LE(value, _headerSize + _writeOffset);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + SIZE_INT64;
|
||||
for (int i = endOffset; i < endOffset + dataLength - SIZE_INT64; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
@@ -179,10 +140,8 @@ namespace AsarSharp.PickleTools
|
||||
|
||||
public bool WriteUInt64(ulong value)
|
||||
{
|
||||
EnsureCapacity(SIZE_UINT64);
|
||||
|
||||
var dataLength = AlignInt(SIZE_UINT64, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
const int dataLength = SIZE_UINT64;
|
||||
int newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
@@ -190,102 +149,69 @@ namespace AsarSharp.PickleTools
|
||||
}
|
||||
|
||||
WriteUInt64LE(value, _headerSize + _writeOffset);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + SIZE_UINT64;
|
||||
for (int i = endOffset; i < endOffset + dataLength - SIZE_UINT64; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteFloat(float value)
|
||||
{
|
||||
EnsureCapacity(SIZE_FLOAT);
|
||||
|
||||
var dataLength = AlignInt(SIZE_FLOAT, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
const int dataLength = SIZE_FLOAT;
|
||||
int newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
|
||||
Array.Copy(bytes, 0, _header, _headerSize + _writeOffset, SIZE_FLOAT);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + SIZE_FLOAT;
|
||||
for (int i = endOffset; i < endOffset + dataLength - SIZE_FLOAT; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
int bits = BitConverter.ToInt32(BitConverter.GetBytes(value), 0);
|
||||
WriteInt32LE(bits, _headerSize + _writeOffset);
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteDouble(double value)
|
||||
{
|
||||
EnsureCapacity(SIZE_DOUBLE);
|
||||
|
||||
var dataLength = AlignInt(SIZE_DOUBLE, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
const int dataLength = SIZE_DOUBLE;
|
||||
int newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
|
||||
Array.Copy(bytes, 0, _header, _headerSize + _writeOffset, SIZE_DOUBLE);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + SIZE_DOUBLE;
|
||||
for (int i = endOffset; i < endOffset + dataLength - SIZE_DOUBLE; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
long bits = BitConverter.DoubleToInt64Bits(value);
|
||||
WriteInt64LE(bits, _headerSize + _writeOffset);
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteString(string value)
|
||||
{
|
||||
byte[] strBytes = Encoding.UTF8.GetBytes(value);
|
||||
int length = strBytes.Length;
|
||||
int byteLen = Encoding.UTF8.GetByteCount(value);
|
||||
|
||||
if (!WriteInt(length))
|
||||
if (!WriteInt(byteLen))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dataLength = AlignInt(length, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
int aligned = AlignInt(byteLen, SIZE_UINT32);
|
||||
int newSize = _writeOffset + aligned;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
Array.Copy(strBytes, 0, _header, _headerSize + _writeOffset, length);
|
||||
int writeStart = _headerSize + _writeOffset;
|
||||
Encoding.UTF8.GetBytes(value, 0, value.Length, _header, writeStart);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + length;
|
||||
for (int i = endOffset; i < endOffset + dataLength - length; i++)
|
||||
// zero alignment padding
|
||||
for (int i = writeStart + byteLen; i < writeStart + aligned; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
@@ -294,132 +220,80 @@ namespace AsarSharp.PickleTools
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public void SetPayloadSize(int payloadSize)
|
||||
{
|
||||
WriteUInt32LE((uint)payloadSize, 0);
|
||||
}
|
||||
|
||||
public int GetPayloadSize()
|
||||
{
|
||||
return (int)ReadUInt32LE(0);
|
||||
}
|
||||
|
||||
|
||||
public int GetPayloadSize() => (int)ReadUInt32LE(0);
|
||||
|
||||
private void Resize(int newCapacity)
|
||||
{
|
||||
newCapacity = AlignInt(newCapacity, PAYLOAD_UNIT);
|
||||
byte[] newHeader = new byte[_header.Length + newCapacity];
|
||||
Array.Copy(_header, 0, newHeader, 0, _header.Length);
|
||||
Buffer.BlockCopy(_header, 0, newHeader, 0, _header.Length);
|
||||
_header = newHeader;
|
||||
_capacityAfterHeader = newCapacity;
|
||||
}
|
||||
|
||||
|
||||
public static int AlignInt(int i, int alignment)
|
||||
{
|
||||
return i + ((alignment - (i % alignment)) % alignment);
|
||||
}
|
||||
|
||||
private void EnsureCapacity(int additionalSize)
|
||||
{
|
||||
var dataLength = AlignInt(additionalSize, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
}
|
||||
|
||||
#region Auxiliary methods for reading/writing values in Little Endian
|
||||
|
||||
private uint ReadUInt32LE(int offset)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
return BitConverter.ToUInt32(_header, offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (uint)(_header[offset] |
|
||||
(_header[offset + 1] << 8) |
|
||||
(_header[offset + 2] << 16) |
|
||||
(_header[offset + 3] << 24));
|
||||
}
|
||||
// _header is allocated by us so always little-endian-friendly when on LE host.
|
||||
return (uint)(_header[offset] |
|
||||
(_header[offset + 1] << 8) |
|
||||
(_header[offset + 2] << 16) |
|
||||
(_header[offset + 3] << 24));
|
||||
}
|
||||
|
||||
private void WriteInt32LE(int value, int offset)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Array.Copy(bytes, 0, _header, offset, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
private void WriteUInt32LE(uint value, int offset)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Array.Copy(bytes, 0, _header, offset, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
private void WriteInt64LE(long value, int offset)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Array.Copy(bytes, 0, _header, offset, 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
_header[offset + 4] = (byte)(value >> 32);
|
||||
_header[offset + 5] = (byte)(value >> 40);
|
||||
_header[offset + 6] = (byte)(value >> 48);
|
||||
_header[offset + 7] = (byte)(value >> 56);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteUInt64LE(ulong value, int offset)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Array.Copy(bytes, 0, _header, offset, 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
_header[offset + 4] = (byte)(value >> 32);
|
||||
_header[offset + 5] = (byte)(value >> 40);
|
||||
_header[offset + 6] = (byte)(value >> 48);
|
||||
_header[offset + 7] = (byte)(value >> 56);
|
||||
}
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
_header[offset + 4] = (byte)(value >> 32);
|
||||
_header[offset + 5] = (byte)(value >> 40);
|
||||
_header[offset + 6] = (byte)(value >> 48);
|
||||
_header[offset + 7] = (byte)(value >> 56);
|
||||
}
|
||||
|
||||
|
||||
private void WriteUInt64LE(ulong value, int offset)
|
||||
{
|
||||
_header[offset] = (byte)value;
|
||||
_header[offset + 1] = (byte)(value >> 8);
|
||||
_header[offset + 2] = (byte)(value >> 16);
|
||||
_header[offset + 3] = (byte)(value >> 24);
|
||||
_header[offset + 4] = (byte)(value >> 32);
|
||||
_header[offset + 5] = (byte)(value >> 40);
|
||||
_header[offset + 6] = (byte)(value >> 48);
|
||||
_header[offset + 7] = (byte)(value >> 56);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AsarSharp.Utils
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute path relative to <paramref name="relativeTo"/>.
|
||||
/// Fast common-case (path is inside relativeTo): plain prefix-strip.
|
||||
/// Falls back to <see cref="Path.GetFullPath"/> + manual relativisation
|
||||
/// when paths must be normalised or '..' segments are required.
|
||||
/// Replaces previous URI-based implementation which was a large hot-path cost.
|
||||
/// </summary>
|
||||
public static string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativeTo))
|
||||
@@ -13,84 +21,134 @@ namespace AsarSharp.Utils
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
var fullRelativeTo = Path.GetFullPath(relativeTo);
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
// Fast path: literal prefix match (no normalisation). Covers ~all
|
||||
// intra-archive callers where both inputs already come from the
|
||||
// same crawl pass.
|
||||
string baseFast = TrimTrailingSeparators(relativeTo);
|
||||
string pathFast = TrimTrailingSeparators(path);
|
||||
|
||||
if (string.Equals(fullRelativeTo, fullPath, StringComparison.OrdinalIgnoreCase))
|
||||
return "";
|
||||
if (string.Equals(baseFast, pathFast, StringComparison.OrdinalIgnoreCase))
|
||||
return string.Empty;
|
||||
|
||||
var relativeToUri = new Uri(fullRelativeTo.EndsWith(Path.DirectorySeparatorChar.ToString())
|
||||
? fullRelativeTo
|
||||
: fullRelativeTo + Path.DirectorySeparatorChar);
|
||||
var pathUri = new Uri(fullPath.EndsWith(Path.DirectorySeparatorChar.ToString()) && !File.Exists(fullPath)
|
||||
? fullPath
|
||||
: fullPath + (Directory.Exists(fullPath) ? Path.DirectorySeparatorChar.ToString() : ""));
|
||||
if (pathFast.Length > baseFast.Length &&
|
||||
pathFast.StartsWith(baseFast, StringComparison.OrdinalIgnoreCase) &&
|
||||
IsSeparator(pathFast[baseFast.Length]))
|
||||
{
|
||||
return pathFast.Substring(baseFast.Length + 1);
|
||||
}
|
||||
|
||||
var relativeUri = relativeToUri.MakeRelativeUri(pathUri);
|
||||
var relativePath = Uri.UnescapeDataString(relativeUri.ToString())
|
||||
.Replace('/', Path.DirectorySeparatorChar);
|
||||
|
||||
return relativePath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
// Slow path: normalise both sides and compute relative — used for
|
||||
// security checks (out-of-tree symlink/destination guards) and the
|
||||
// rare "go up" case.
|
||||
return GetRelativePathNormalised(relativeTo, path);
|
||||
}
|
||||
|
||||
|
||||
private static string GetRelativePathNormalised(string relativeTo, string path)
|
||||
{
|
||||
string fullBase = Path.GetFullPath(relativeTo);
|
||||
string fullPath = Path.GetFullPath(path);
|
||||
|
||||
fullBase = TrimTrailingSeparators(fullBase);
|
||||
fullPath = TrimTrailingSeparators(fullPath);
|
||||
|
||||
if (string.Equals(fullBase, fullPath, StringComparison.OrdinalIgnoreCase))
|
||||
return string.Empty;
|
||||
|
||||
if (fullPath.Length > fullBase.Length &&
|
||||
fullPath.StartsWith(fullBase, StringComparison.OrdinalIgnoreCase) &&
|
||||
IsSeparator(fullPath[fullBase.Length]))
|
||||
{
|
||||
return fullPath.Substring(fullBase.Length + 1);
|
||||
}
|
||||
|
||||
// Need to walk up the common ancestor.
|
||||
char sep = Path.DirectorySeparatorChar;
|
||||
string[] baseParts = fullBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
string[] pathParts = fullPath.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
int common = 0;
|
||||
int max = Math.Min(baseParts.Length, pathParts.Length);
|
||||
while (common < max &&
|
||||
string.Equals(baseParts[common], pathParts[common], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
common++;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = common; i < baseParts.Length; i++)
|
||||
{
|
||||
if (sb.Length > 0) sb.Append(sep);
|
||||
sb.Append("..");
|
||||
}
|
||||
for (int i = common; i < pathParts.Length; i++)
|
||||
{
|
||||
if (sb.Length > 0) sb.Append(sep);
|
||||
sb.Append(pathParts[i]);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string TrimTrailingSeparators(string s)
|
||||
{
|
||||
int end = s.Length;
|
||||
while (end > 0 && IsSeparator(s[end - 1])) end--;
|
||||
return end == s.Length ? s : s.Substring(0, end);
|
||||
}
|
||||
|
||||
private static bool IsSeparator(char c) => c == '/' || c == '\\';
|
||||
|
||||
public static string GetDirectoryName(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return ".";
|
||||
|
||||
string result = Path.GetDirectoryName(path);
|
||||
|
||||
// If the result is an empty string, return “.” as in Node.js
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
return ".";
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static void CopyDirectory(string sourceDir, string destinationDir)
|
||||
{
|
||||
// Create the destination directory
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
// Get all files in the source directory
|
||||
foreach (var file in Directory.GetFiles(sourceDir))
|
||||
{
|
||||
var destFile = Path.Combine(destinationDir, Path.GetFileName(file));
|
||||
File.Copy(file, destFile, true);
|
||||
}
|
||||
|
||||
// Recursively copy all subdirectories
|
||||
foreach (var dir in Directory.GetDirectories(sourceDir))
|
||||
{
|
||||
var destDir = Path.Combine(destinationDir, Path.GetFileName(dir));
|
||||
CopyDirectory(dir, destDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static string GetBasePath(string dir)
|
||||
{
|
||||
// Look for the last path delimiter before any pattern
|
||||
int wildcardIndex = dir.IndexOfAny(new[] { '*', '?' });
|
||||
if (wildcardIndex == -1)
|
||||
{
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
int lastSeparatorIndex = dir.LastIndexOf(Path.DirectorySeparatorChar, wildcardIndex);
|
||||
if (lastSeparatorIndex == -1)
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
|
||||
return dir.Substring(0, lastSeparatorIndex);
|
||||
}
|
||||
|
||||
|
||||
public static void SetUnixFilePermission(string filePath, string permission)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return;
|
||||
|
||||
// Use chmod
|
||||
var process = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
@@ -105,14 +163,12 @@ namespace AsarSharp.Utils
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static void CreateSymbolicLink(string linkTarget, string linkPath)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// On Windows, creating symlinks requires special privileges,
|
||||
// so on many systems it simply won't work without administrator privileges
|
||||
NativeMethods.CreateSymbolicLink(linkPath, linkTarget,
|
||||
Directory.Exists(linkTarget)
|
||||
? NativeMethods.SymLinkFlag.Directory
|
||||
@@ -120,7 +176,6 @@ namespace AsarSharp.Utils
|
||||
return;
|
||||
}
|
||||
|
||||
// In Unix systems we use the corresponding system call
|
||||
var process = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
@@ -135,11 +190,11 @@ namespace AsarSharp.Utils
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static bool IsWindowsPlatform()
|
||||
{
|
||||
return Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
# Changelog
|
||||
|
||||
This file is the source of truth for release notes.
|
||||
The newest entry must match the version in `WandEnhancer/Properties/AssemblyInfo.cs`.
|
||||
|
||||
## [1.0.9.1] - 2026-06-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed Pro features disappearing after a day or two when Wand refreshed account data in the background; account store updates now preserve the patched active subscription by @Kava-4 in #110. Related issue #106
|
||||
- Fixed the new Pro account reducer guard so normal account updates do not fail while keeping Pro active.
|
||||
|
||||
## [1.0.9.0] - 2026-06-15
|
||||
|
||||
### Features
|
||||
|
||||
- The Remote Web Panel now shows mod names, descriptions, and instructions translated to your WeMod account language by @YifePlayte in #98. Related issue: #85
|
||||
- Added a language selector to the Remote Web Panel (English, Russian, German, French, Spanish, Simplified Chinese) with automatic detection from the browser language.
|
||||
|
||||
### Improvements
|
||||
|
||||
- Release builds are now code-signed, which reduces false-positive antivirus and VirusTotal detections.
|
||||
- Reworked the Remote Web Panel internals around feature capabilities for easier maintenance, with no change to existing behavior.
|
||||
|
||||
## [1.0.8.4] - 2026-06-10
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed QR code issues on the latest Wand version.
|
||||
- Fixed application hang that occurred after Wand updates with pending patches.
|
||||
|
||||
## [1.0.8.3] - 2026-06-06
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed the Remote Web Panel patches so they reliably apply on newer Wand builds by making the remote bridge patch anchors version-resilient.
|
||||
- Fixed Pro activation being lost after changing the app language; the account language endpoint now keeps the patched subscription.
|
||||
- Fixed "WeMod directory not found" when Wand/WeMod is installed outside the default location or only one brand folder exists. The patcher now also resolves the install directory from a running Wand/WeMod process. #82
|
||||
- Hid the Pro "Remote" onboarding card in the Explore Pro benefits dialog. #86
|
||||
|
||||
## [1.0.8.2] - 2026-05-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- Rolled back an incorrect Disable Updates patch fix that introduced a `SyntaxError` preventing Wand from launching.
|
||||
|
||||
## [1.0.8.1] - 2026-05-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed a syntax error in the Disable Updates patch that prevented Wand from launching. #70
|
||||
- Fixed an issue where the Remote Web Panel WebSocket connection wouldn't automatically reconnect when turning returning to the app or turning on the screen.
|
||||
- Reduced battery consumption and device heating on mobile device by optimizing heavy UI blur effects and eliminating unnecessary React re-renders in the Remote Web Panel. #67
|
||||
|
||||
## [1.0.8.0] - 2026-05-06
|
||||
|
||||
### Features
|
||||
|
||||
- Added the My Games list to the Remote Panel with remote start and stop actions.
|
||||
- Improved the Remote Panel with new UI and overall UX.
|
||||
- Added an update dialog with release notes and access to full patch notes.
|
||||
|
||||
### Improvements
|
||||
|
||||
- Optimized and sped up patching and ASAR unpack/pack operations.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed in-place handling of unpacked `app.asar.unpacked` assets during packing to avoid locked-file failures.
|
||||
- Fixed local network IP detection for QR-based Remote Panel pairing, so the app no longer picks Cloudflare, VMware, and similar non-LAN adapters by mistake.
|
||||
|
||||
## [1.0.7.0] - 2026-05-01
|
||||
|
||||
### Features
|
||||
|
||||
- New Remote Web Panel: control local app features from a phone or another PC over the local network via QR code connection. #37
|
||||
- Custom Script Loader: inject and execute custom user `.js` scripts directly into the Wand renderer process via the patch modal.
|
||||
- Added the ability to export and copy application logs from the UI.
|
||||
- Stabilized the DevTools on `F12` patch.
|
||||
- Added a repository mirror on GitLab. #47
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed ASAR unpacking failures on locked files or missing entries. #63 #57
|
||||
|
||||
## [1.0.6.0] - 2025-12-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed issues related to Wand `12.5.1`. #35
|
||||
- Fixed a bug where the patch could not be reapplied after restoring without restarting the patcher.
|
||||
- Removed the redundant telemetry removal option from patch settings.
|
||||
|
||||
### Features
|
||||
|
||||
- Added localization support.
|
||||
- Added the patch option to open Wand DevTools with `F12`.
|
||||
|
||||
## [1.0.5.0] - 2025-11-30
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed the issue where games detected the debugger. #33 #23 #19 #13
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed patch methods.
|
||||
- Removed shortcut launch.
|
||||
- Removed automatic patching for new versions because it is not compatible with the current patch method.
|
||||
|
||||
### Notes
|
||||
|
||||
- This version is incompatible with previous versions of the patcher. Before updating, previous patches must be rolled back.
|
||||
- With the current method, the patcher only needs to be run once to apply the patch.
|
||||
- Thanks to issue #12 for sharing the patching method used here.
|
||||
|
||||
## [1.0.4.0] - 2025-11-05
|
||||
|
||||
### Features
|
||||
|
||||
- Added backward compatibility for older WeMod versions so both legacy WeMod and the newer Wand builds can be patched.
|
||||
- Added manual version management for patches, including separate patches and shortcuts for individual WeMod or Wand versions.
|
||||
|
||||
## [1.0.3.0] - 2025-11-03
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed issues related to the WeMod to Wand rebrand. #24
|
||||
|
||||
## [1.0.2.0] - 2025-04-09
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed a performance issue when a process with an applied patch was scanned again.
|
||||
- Fixed exception propagation into the WeMod process. #11
|
||||
|
||||
## [1.0.1.0] - 2025-04-01
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed WeMod overlay breakage when using the runtime patch.
|
||||
|
||||
## [1.0.0.0] - 2025-03-24
|
||||
|
||||
### Changes
|
||||
|
||||
- Replaced Electron with WPF.
|
||||
- Reduced the `.exe` size by more than 70x.
|
||||
- Updated the UI.
|
||||
- Added two types of patching.
|
||||
- Fixed hotkeys breaking after patching.
|
||||
- Added patch recovery.
|
||||
- Removed external dependencies such as Electron and ASAR tooling from runtime.
|
||||
- Added a patch option to disable WeMod updates.
|
||||
|
||||
### Notes
|
||||
|
||||
- VirusTotal detection increased with the new patching method.
|
||||
|
||||
## [0.0.1] - 2025-01-04
|
||||
|
||||
### Changes
|
||||
|
||||
- Basic ElectronJS wrapper over the original script.
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# 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)
|
||||
- [Release Process](#release-process)
|
||||
- [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.
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Update `WandEnhancer/Properties/AssemblyInfo.cs`.
|
||||
2. Add a new top section with the same version to `CHANGELOG.md`.
|
||||
3. Configure local hooks once:
|
||||
```
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
4. Commit and push the version/changelog changes.
|
||||
5. Create and push a tag matching the same version exactly, for example:
|
||||
```
|
||||
git tag 1.0.8.0
|
||||
git push origin 1.0.8.0
|
||||
```
|
||||
6. GitHub Actions will validate the version, build the project, extract the matching changelog section, and publish the release automatically.
|
||||
|
||||
## 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!
|
||||
@@ -2,58 +2,126 @@
|
||||
|
||||

|
||||
|
||||
---
|
||||
<h1>WeMod Patcher</h1>
|
||||
# WandEnhancer
|
||||
|
||||
[](https://gitlab.com/kitbyte/wand-enhancer)
|
||||
[](https://www.virustotal.com/gui/file/f6897cf583e9f8ea11e0ee4c3fb99b86c50336b28de706e3e0b9181b4e3cf223)
|
||||
|
||||
</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 **same local network**. Some routers and guest Wi-Fi networks enable client isolation/AP isolation, which blocks devices on the same SSID from reaching each other. If it still does not load, check Windows Firewall and allow inbound traffic on TCP port `3223` for your local network. If Windows marked your connection as **Public**, switching it to **Private** can also help.
|
||||
- **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 binary release.
|
||||
3. Run the enhancer to apply local client modifications.
|
||||
|
||||
> Source archives are intended for developers who want to build the project locally. They are not prebuilt binaries.
|
||||
|
||||
## 🧩 Custom scripts
|
||||
|
||||
You can inject your own JavaScript into Wand at patch time to tweak or fix things in the client UI. This reuses the same renderer injection the Remote Web Panel uses, so it requires the **Remote Web Panel** patch to be enabled.
|
||||
|
||||
**How to add a script**
|
||||
|
||||
- In the patch dialog, add one or more `.js` files (only existing `.js` files are accepted), **or**
|
||||
- Drop `.js` files into a `renderer-scripts/` folder placed next to the patcher executable.
|
||||
|
||||
Then patch as usual — your scripts are bundled into the client and run inside Wand's window.
|
||||
|
||||
**How it runs**
|
||||
|
||||
- Each script runs inside Wand's renderer (full DOM access, plus Node `require`).
|
||||
- It is wrapped so a thrown error is logged and never crashes Wand.
|
||||
- It may run **more than once** per launch (on load and again shortly after), so guard one‑time work behind a global flag.
|
||||
- A small `WandEnhancer` helper is available: `WandEnhancer.log(...)`, `WandEnhancer.remoteUrl`, `WandEnhancer.apiVersion`.
|
||||
|
||||
**Minimal example** (`hello.js`)
|
||||
|
||||
```js
|
||||
// Injected scripts can run multiple times — guard one-time setup.
|
||||
if (!globalThis.__helloScriptInstalled) {
|
||||
globalThis.__helloScriptInstalled = true;
|
||||
|
||||
WandEnhancer.log("Hello from my custom script!", WandEnhancer.remoteUrl);
|
||||
|
||||
new MutationObserver(() => {
|
||||
const dialog = document.querySelector("ux-dialog:not([data-seen])");
|
||||
if (dialog) {
|
||||
dialog.setAttribute("data-seen", "1");
|
||||
WandEnhancer.log("A dialog opened.");
|
||||
}
|
||||
}).observe(document.documentElement, { childList: true, subtree: true });
|
||||
}
|
||||
```
|
||||
|
||||
> Scripts run with the same privileges as the Wand client. Only add scripts you trust and understand.
|
||||
|
||||
## 🛠️ How to build from source
|
||||
|
||||
Building from source on Windows requires a local development environment.
|
||||
|
||||
### Requirements
|
||||
|
||||
- `CMake`
|
||||
- `Node.js` and `pnpm`
|
||||
- `Visual Studio 2022` or `Build Tools for Visual Studio 2022` with `MSBuild`
|
||||
- Visual Studio `Desktop development with C++` workload
|
||||
- .NET Framework 4.8 desktop build tools / targeting pack
|
||||
|
||||
### Build steps
|
||||
|
||||
1. Clone this repository.
|
||||
2. Install the requirements above and make sure `cmake`, `pnpm`, and `MSBuild` are available.
|
||||
3. Run `build.cmd` from Command Prompt or PowerShell.
|
||||
|
||||
The build script installs the web panel dependencies, builds the frontend, compiles the native helper with CMake, restores NuGet packages, and builds the WPF solution.
|
||||
|
||||
---
|
||||
|
||||
## ❓ 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
|
||||

|
||||
<div align='center'>
|
||||
|
||||

|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
@@ -61,8 +129,20 @@ This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.md) fi
|
||||
|
||||
---
|
||||
## ❤️ Support
|
||||
[](https://ko-fi.com/kitbyte)
|
||||
|
||||
If you find this project useful, you can support its development using any of the options below 🙌
|
||||
|
||||
[](https://www.patreon.com/kitbyte/gift)
|
||||
[](https://tronscan.org/#/address/TQdvau8pAy5Tg1Aa588tTcPCFgbcHtuoxc)
|
||||
[](https://www.blockchain.com/explorer/addresses/btc/1EZKDcyU8REm9JW5xwXJqSpn5Xaq5yAWWX)
|
||||
[](https://etherscan.io/address/0xd904d9d0557f88bbb1c4ab3582b4ca0d8a730e8d)
|
||||
|
||||
|
||||
---
|
||||
|
||||
[](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.
|
||||
|
||||
---
|
||||
|
||||
[](https://www.star-history.com/#k1tbyte/Wand-Enhancer&Date)
|
||||
@@ -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)
|
||||
+1
-1
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace WeModPatcher.Converters
|
||||
namespace WandEnhancer.Converters
|
||||
{
|
||||
internal sealed class ToVisibilityConverter : BaseBooleanConverter<Visibility>
|
||||
{
|
||||
@@ -0,0 +1,506 @@
|
||||
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 LocalCustomScriptsDirectoryName = "renderer-scripts";
|
||||
private const string RemotePanelDirectoryName = "remote-panel";
|
||||
private const string RemoteBridgeTargetFileName = "bridge.cjs";
|
||||
private const string RemoteRendererScriptsDirectoryName = "renderer-scripts";
|
||||
private const string EmbeddedRemotePanelDistPrefix = "remote-panel/dist/";
|
||||
private const string AppBundleFilePrefix = "app-";
|
||||
private const string AppBundleFileSuffix = ".bundle.js";
|
||||
private const string IndexBundleFileName = "index.js";
|
||||
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, out bool patchApplied)
|
||||
{
|
||||
patchApplied = false;
|
||||
|
||||
if (patch.Applied)
|
||||
{
|
||||
return js;
|
||||
}
|
||||
|
||||
if (!CanSearchPatchInFile(fileName, patch) || !ContainsSearchHint(js, patch.SearchHints))
|
||||
{
|
||||
return js;
|
||||
}
|
||||
|
||||
var match = patch.Target.Match(js);
|
||||
if (!match.Success)
|
||||
{
|
||||
return js;
|
||||
}
|
||||
|
||||
var prefix = $"[ENHANCER] [{patchType} -> {patch.Name}]";
|
||||
|
||||
if(patch.SingleMatch && match.NextMatch().Success)
|
||||
{
|
||||
throw new Exception(
|
||||
$"{prefix} Patch failed. Multiple target functions found. Looks like the version is not supported");
|
||||
}
|
||||
|
||||
string patchSource = patch.PatchFactory != null
|
||||
? patch.PatchFactory(match)
|
||||
: patch.Patch;
|
||||
|
||||
if (patch.Resolver != null)
|
||||
{
|
||||
string resolvedField = patch.Resolver.Handler(match.Value);
|
||||
if (string.IsNullOrEmpty(resolvedField))
|
||||
{
|
||||
throw new Exception($"{prefix} Resolver failed to find field name");
|
||||
}
|
||||
|
||||
patchSource = patchSource.Replace(patch.Resolver.Placeholder, resolvedField);
|
||||
}
|
||||
|
||||
_logger($"{prefix} Found target function in: " + Path.GetFileName(fileName), ELogType.Info);
|
||||
|
||||
string newJs;
|
||||
if (patch.PatchFactory != null)
|
||||
{
|
||||
newJs = patch.SingleMatch
|
||||
? patch.Target.Replace(js, _ => patchSource, 1)
|
||||
: patch.Target.Replace(js, _ => patchSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
newJs = patch.SingleMatch
|
||||
? patch.Target.Replace(js, patchSource, 1)
|
||||
: patch.Target.Replace(js, patchSource);
|
||||
}
|
||||
|
||||
_logger($"{prefix} Patch applied", ELogType.Success);
|
||||
patch.Applied = true;
|
||||
patchApplied = true;
|
||||
|
||||
return newJs;
|
||||
}
|
||||
|
||||
private void PatchAsar()
|
||||
{
|
||||
var items = Directory.EnumerateFiles(_unpackedPath, $"*{JavaScriptFileExtension}", SearchOption.TopDirectoryOnly)
|
||||
.Where(IsCandidateBundleFile)
|
||||
.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;
|
||||
}
|
||||
|
||||
if (!CouldFileContainRemainingPatch(item, remainingPatches, enhancerConfig))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string data = File.ReadAllText(item);
|
||||
bool fileChanged = false;
|
||||
|
||||
foreach (var entry in remainingPatches.ToList())
|
||||
{
|
||||
var entries = enhancerConfig[entry];
|
||||
foreach (var patchEntry in entries)
|
||||
{
|
||||
bool patchApplied;
|
||||
data = ApplyJsPatch(item, data, patchEntry, entry, out patchApplied);
|
||||
fileChanged = fileChanged || patchApplied;
|
||||
}
|
||||
|
||||
if (entries.All(x => x.Applied))
|
||||
{
|
||||
remainingPatches.Remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileChanged)
|
||||
{
|
||||
File.WriteAllText(item, data);
|
||||
}
|
||||
}
|
||||
|
||||
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 bool IsCandidateBundleFile(string filePath)
|
||||
{
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
return fileName.Equals(IndexBundleFileName, StringComparison.OrdinalIgnoreCase)
|
||||
|| (fileName.StartsWith(AppBundleFilePrefix, StringComparison.OrdinalIgnoreCase)
|
||||
&& fileName.EndsWith(AppBundleFileSuffix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool CouldFileContainRemainingPatch(string filePath, IEnumerable<EPatchType> remainingPatches, Dictionary<EPatchType, EnhancerConfig.PatchEntry[]> enhancerConfig)
|
||||
{
|
||||
foreach (var patchType in remainingPatches)
|
||||
{
|
||||
foreach (var patchEntry in enhancerConfig[patchType])
|
||||
{
|
||||
if (patchEntry.Applied)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CanSearchPatchInFile(filePath, patchEntry))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CanSearchPatchInFile(string filePath, EnhancerConfig.PatchEntry patch)
|
||||
{
|
||||
if (patch.CandidateFileNames == null || patch.CandidateFileNames.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string fileName = Path.GetFileName(filePath);
|
||||
return patch.CandidateFileNames.Any(candidate => fileName.Equals(candidate, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool ContainsSearchHint(string source, string[] searchHints)
|
||||
{
|
||||
if (searchHints == null || searchHints.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return searchHints.Any(searchHint => source.IndexOf(searchHint, StringComparison.Ordinal) >= 0);
|
||||
}
|
||||
|
||||
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 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 (!File.Exists(targetBridgePath))
|
||||
{
|
||||
throw new FileNotFoundException("[ENHANCER] Remote bridge artifact is missing. Run `cd web-panel && pnpm run build` before patching.", targetBridgePath);
|
||||
}
|
||||
|
||||
int defaultScriptCount = Directory.Exists(targetScriptsRoot)
|
||||
? Directory.GetFiles(targetScriptsRoot, JavaScriptFileSearchPattern, SearchOption.TopDirectoryOnly).Length
|
||||
: 0;
|
||||
if (defaultScriptCount == 0)
|
||||
{
|
||||
throw new FileNotFoundException("[ENHANCER] Remote renderer script artifacts are missing. Run `cd web-panel && pnpm run build` before patching.", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using WandEnhancer.Models;
|
||||
|
||||
namespace WandEnhancer.Core
|
||||
{
|
||||
public static class EnhancerConfig
|
||||
{
|
||||
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 Func<Match, string> PatchFactory { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool Applied { get; set; }
|
||||
public bool SingleMatch { get; set; } = true;
|
||||
public string[] CandidateFileNames { get; set; }
|
||||
public string[] SearchHints { get; set; }
|
||||
public ResolveContext Resolver { get; set; }
|
||||
}
|
||||
|
||||
private static string RequireGroup(Match match, string groupName, string patchName)
|
||||
{
|
||||
var group = match.Groups[groupName];
|
||||
if (!group.Success || string.IsNullOrEmpty(group.Value))
|
||||
{
|
||||
throw new Exception($"{patchName} failed to resolve {groupName}");
|
||||
}
|
||||
|
||||
return group.Value;
|
||||
}
|
||||
|
||||
private static string RequirePattern(string source, string pattern, string groupName, string patchName)
|
||||
{
|
||||
var match = Regex.Match(source, pattern, RegexOptions.Singleline);
|
||||
return RequireGroup(match, groupName, patchName);
|
||||
}
|
||||
|
||||
private static string BuildSetAccountLanguagePatch(Match match)
|
||||
{
|
||||
var parameters = RequireGroup(match, "params", "setAccountLanguage");
|
||||
var expr = RequireGroup(match, "expr", "setAccountLanguage");
|
||||
return $"setAccountLanguage({parameters}){{return ({expr}).then(response=>{{response&&\"object\"==typeof response&&(response.subscription={{period:\"yearly\",state:\"active\"}});return response;}})}}";
|
||||
}
|
||||
|
||||
private static string BuildSetAccountReducerPatch(Match match)
|
||||
{
|
||||
var decl = RequireGroup(match, "decl", "setAccountReducer");
|
||||
var fn = RequireGroup(match, "fn", "setAccountReducer");
|
||||
var parameters = RequireGroup(match, "params", "setAccountReducer");
|
||||
var state = RequireGroup(match, "state", "setAccountReducer");
|
||||
var account = RequireGroup(match, "account", "setAccountReducer");
|
||||
return
|
||||
$"const {decl}=\"ACTION_SET_ACCOUNT\";function {fn}({parameters}){{const a={account}&&\"object\"==typeof {account}?{{...{account},subscription:{{period:\"yearly\",state:\"active\"}}}}:{account};return{{...{state},account:a}}}}";
|
||||
}
|
||||
|
||||
private static string BuildRemoteBridgeResetPatch(Match match)
|
||||
{
|
||||
var source = match.Value;
|
||||
var method = RequireGroup(match, "method", "remoteBridgeReset");
|
||||
var disposableField = RequirePattern(source, @"this\.(?<disposable>#[\w$]+)\s*&&\s*\(\s*this\.\k<disposable>\.dispose\(\)", "disposable", "remoteBridgeReset");
|
||||
var instanceField = RequirePattern(source, @"this\.(?<instance>#[\w$]+)\s*=\s*Date\.now\(\)\.toString\(\)", "instance", "remoteBridgeReset");
|
||||
var trainerIdField = RequirePattern(source, @"Date\.now\(\)\.toString\(\)\s*\)?\s*,\s*\(?\s*this\.(?<trainerId>#[\w$]+)\s*=\s*null", "trainerId", "remoteBridgeReset");
|
||||
var supportedVersionsField = RequirePattern(source, @"this\.(?<versions>#[\w$]+)\s*=\s*\[\]", "versions", "remoteBridgeReset");
|
||||
var trainerField = RequirePattern(source, @"this\.(?<versions>#[\w$]+)\s*=\s*\[\]\s*\)?\s*,\s*\(?\s*this\.(?<trainer>#[\w$]+)\s*=\s*null", "trainer", "remoteBridgeReset");
|
||||
|
||||
return $"{method}(){{this.{disposableField}&&(this.{disposableField}.dispose(),this.{disposableField}=null),this.{instanceField}=Date.now().toString(),this.{trainerIdField}=null,this.{supportedVersionsField}=[],this.{trainerField}=null,this.__wandRemoteTrainerInfo=null,this.__wandRemoteBridge?.sync(null)}}";
|
||||
}
|
||||
|
||||
private static string BuildRemoteBridgeSyncSnapshotPatch(Match match)
|
||||
{
|
||||
var source = match.Value;
|
||||
var method = RequireGroup(match, "method", "remoteBridgeSyncSnapshot");
|
||||
var statusAlias = RequirePattern(source, @"this\.status\s*===\s*(?<value>[\w$]+)\.Connected", "value", "remoteBridgeSyncSnapshot");
|
||||
var trainerField = RequirePattern(source, @"this\.(?<trainer>#[\w$]+)\?\.\s*getMetadata\s*\(\s*(?<metadata>[\w$]+\.[\w$]+)\s*\)\?\.\s*gameVersion", "trainer", "remoteBridgeSyncSnapshot");
|
||||
var metadataExport = RequirePattern(source, @"this\.(?<trainer>#[\w$]+)\?\.\s*getMetadata\s*\(\s*(?<metadata>[\w$]+\.[\w$]+)\s*\)\?\.\s*gameVersion", "metadata", "remoteBridgeSyncSnapshot");
|
||||
var notesField = RequirePattern(source, @"this\.(?<notes>#[\w$]+)\s*\[\s*this\.(?<trainerId>#[\w$]+)\s*\?\?\s*""""\s*\]", "notes", "remoteBridgeSyncSnapshot");
|
||||
var trainerIdField = RequirePattern(source, @"this\.(?<notes>#[\w$]+)\s*\[\s*this\.(?<trainerId>#[\w$]+)\s*\?\?\s*""""\s*\]", "trainerId", "remoteBridgeSyncSnapshot");
|
||||
var gameField = RequirePattern(source, @"this\.(?<game>#[\w$]+)\s*&&.*?getPreferredInstallationInfo\s*\(\s*this\.\k<game>\s*\)", "game", "remoteBridgeSyncSnapshot");
|
||||
var installationField = RequirePattern(source, @"this\.(?<game>#[\w$]+)\s*&&.*?this\.(?<installation>#[\w$]+)\.getPreferredInstallationInfo\s*\(\s*this\.\k<game>\s*\)", "installation", "remoteBridgeSyncSnapshot");
|
||||
var supportedVersionsField = RequirePattern(source, @"!\s*this\.(?<versions>#[\w$]+)\.includes\s*\(\s*[\w$]+\.version\s*\)", "versions", "remoteBridgeSyncSnapshot");
|
||||
var remoteChannelField = RequirePattern(source, @"this\.(?<remote>#[\w$]+)\?\.\s*send\s*\(\s*""client-state""", "remote", "remoteBridgeSyncSnapshot");
|
||||
var valuesMethod = RequirePattern(source, @"values\s*:\s*this\.(?<values>#[\w$]+)\s*\(\s*\)", "values", "remoteBridgeSyncSnapshot");
|
||||
var instanceField = RequirePattern(source, @"instanceId\s*:\s*this\.(?<instance>#[\w$]+)", "instance", "remoteBridgeSyncSnapshot");
|
||||
var themeField = RequirePattern(source, @"themeId\s*:\s*this\.(?<theme>#[\w$]+)", "theme", "remoteBridgeSyncSnapshot");
|
||||
var settingsHelper = RequirePattern(source, @"settings\s*:\s*(?<settings>[\w$]+)\s*\(\s*this\.settings\s*\)", "settings", "remoteBridgeSyncSnapshot");
|
||||
var languageField = RequirePattern(source, @"language\s*:\s*this\.(?<language>#[\w$]+)", "language", "remoteBridgeSyncSnapshot");
|
||||
var timerField = RequirePattern(source, @"isTimeLimitExpired\s*:\s*""expired""\s*===\s*this\.(?<timer>#[\w$]+)\.timerState", "timer", "remoteBridgeSyncSnapshot");
|
||||
|
||||
return $"{method}(){{let e,t=!1,s=this.{trainerField}?.getMetadata({metadataExport})?.gameVersion??null,o=!1;const n=this.{notesField}[this.{trainerIdField}??\"\"]||null;this.{gameField}&&(e=this.{installationField}.getPreferredInstallationInfo(this.{gameField}),e.app&&(t=!0,s??=e.version??null,o=\"number\"==typeof e.version&&!this.{supportedVersionsField}.includes(e.version)));this.status==={statusAlias}.Connected&&this.{remoteChannelField}?.send(\"client-state\",{{instanceId:this.{instanceField},trainerId:this.{trainerIdField},trainerLoading:this.{trainerField}?.isLoading(),gameInstalled:t,gameVersion:s,needsCompatibilityWarning:o,values:this.{valuesMethod}(),themeId:this.{themeField},settings:{settingsHelper}(this.settings),language:this.{languageField},accountUuid:this.account.uuid,notesReadHash:n,isTimeLimitExpired:\"expired\"===this.{timerField}.timerState}});this.__wandRemoteBridge?.sync({{instanceId:this.{instanceField},trainerId:this.{trainerIdField},trainerInfo:this.__wandRemoteTrainerInfo??null,metadata:this.{trainerField}?.getMetadata({metadataExport})??null,trainerLoading:this.{trainerField}?.isLoading()??false,gameInstalled:t,gameVersion:s,needsCompatibilityWarning:o,language:this.{languageField},themeId:this.{themeField},notesReadHash:n,isTimeLimitExpired:\"expired\"===this.{timerField}.timerState,values:this.{valuesMethod}()}})}}";
|
||||
}
|
||||
|
||||
public static Dictionary<EPatchType, PatchEntry[]> GetInstance()
|
||||
{
|
||||
return new Dictionary<EPatchType, PatchEntry[]>()
|
||||
{
|
||||
{
|
||||
EPatchType.ActivatePro,
|
||||
new[]
|
||||
{
|
||||
new PatchEntry
|
||||
{
|
||||
SearchHints = new[] { "getUserAccount()", "/v3/account" },
|
||||
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
|
||||
{
|
||||
SearchHints = new[] { "setAccountWandBrandExperience()", "/v3/account/brand_experience_wand" },
|
||||
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;})}"
|
||||
},
|
||||
new PatchEntry
|
||||
{
|
||||
// Account-returning endpoint the original patches missed: changing
|
||||
// language dispatches its (non-Pro) response into the store and
|
||||
// wiped Pro. Wrap the result the same way. Param names are captured
|
||||
// so the rewritten body keeps the real argument identifiers.
|
||||
Name = "setAccountLanguage",
|
||||
SearchHints = new[] { "setAccountLanguage(", "/v3/account/language" },
|
||||
Target = new Regex(
|
||||
@"setAccountLanguage\((?<params>[^)]*)\)\{\s*return\s+(?<expr>this\.#\w+\.post\(""/v3/account/language"",\{[^}]*\}\))\s*;?\s*\}",
|
||||
RegexOptions.Singleline),
|
||||
PatchFactory = BuildSetAccountLanguagePatch
|
||||
},
|
||||
new PatchEntry
|
||||
{
|
||||
// Last-resort guard: any code path that dispatches ACTION_SET_ACCOUNT
|
||||
// (periodic refreshAccount, push updates, profile edits, etc.) must keep
|
||||
// subscription on the store object even when it bypasses the account API
|
||||
// service methods patched above.
|
||||
Name = "setAccountReducer",
|
||||
SearchHints = new[] { "ACTION_SET_ACCOUNT" },
|
||||
Target = new Regex(
|
||||
@"const (?<decl>\w+)=""ACTION_SET_ACCOUNT"";function (?<fn>\w+)\((?<params>[^)]*)\)\{return\{\.\.\.(?<state>\w+),account:(?<account>\w+)\}\}",
|
||||
RegexOptions.Singleline),
|
||||
PatchFactory = BuildSetAccountReducerPatch
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
EPatchType.DisableUpdates,
|
||||
new[]
|
||||
{
|
||||
// Regex consumes 4 closing parens (`)))) `); the 5th (registerHandler's own close)
|
||||
// remains in the original file after replacement. Patch must end with 3 parens — NOT 4.
|
||||
new PatchEntry
|
||||
{
|
||||
CandidateFileNames = new[] { "index.js" },
|
||||
SearchHints = new[] { "ACTION_CHECK_FOR_UPDATE" },
|
||||
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",
|
||||
CandidateFileNames = new[] { "index.js" },
|
||||
SearchHints = new[] { "whenReady().then(" },
|
||||
// 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",
|
||||
CandidateFileNames = new[] { "index.js" },
|
||||
SearchHints = new[] { "whenReady().then(run)" },
|
||||
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",
|
||||
SearchHints = new[] { "client-state" },
|
||||
Target = new Regex(@"(?<method>#[\w$]+)\(\)\s*\{\s*(?<body>(?:(?!__wandRemoteBridge|}\s*#[\w$]+\(\)).)*?Date\.now\(\)\.toString\(\)(?:(?!__wandRemoteBridge|}\s*#[\w$]+\(\)).)*?\[\](?:(?!__wandRemoteBridge|}\s*#[\w$]+\(\)).)*?)\s*\}\s*(?=#[\w$]+\(\)\s*\{\s*if\s*\(\s*this\.status\s*===\s*[\w$]+\.Connected\s*\).*?""client-state"")",
|
||||
RegexOptions.Singleline),
|
||||
PatchFactory = BuildRemoteBridgeResetPatch
|
||||
},
|
||||
new PatchEntry
|
||||
{
|
||||
Name = "remoteBridgeSyncSnapshot",
|
||||
SearchHints = new[] { "client-state" },
|
||||
Target = new Regex(@"(?<method>#[\w$]+)\(\)\s*\{\s*if\s*\(\s*this\.status\s*===\s*[\w$]+\.Connected\s*\)\s*\{(?<body>.*?""client-state"".*?isTimeLimitExpired\s*:\s*""expired""\s*===\s*this\.\#[\w$]+\.timerState.*?\)\s*;?\s*\)?\s*;?)\s*\}\s*\}(?=\s*#[\w$]+\(\)\s*\{\s*if\s*\(\s*!this\.\#[\w$]+\?\.\s*isActive\(\)\s*\)\s*return\s*null)",
|
||||
RegexOptions.Singleline),
|
||||
PatchFactory = BuildRemoteBridgeSyncSnapshotPatch
|
||||
},
|
||||
new PatchEntry
|
||||
{
|
||||
Name = "remoteBridgeBindHandler",
|
||||
SearchHints = new[] { "client-state" },
|
||||
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",
|
||||
SearchHints = new[] { "client-value-changed" },
|
||||
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()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Aktuelle Version</s:String>
|
||||
<s:String x:Key="up_latest_version">Neueste Version</s:String>
|
||||
<s:String x:Key="up_release_notes">Versionshinweise</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Für diese Version sind keine Versionshinweise verfügbar.</s:String>
|
||||
<s:String x:Key="up_show_more">Gesamtes Changelog anzeigen</s:String>
|
||||
<s:String x:Key="up_show_less">Nur aktuelle Hinweise anzeigen</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Changelog wird geladen...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Das vollständige Changelog konnte nicht geladen werden. Stattdessen werden die aktuellen Hinweise angezeigt.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Current version</s:String>
|
||||
<s:String x:Key="up_latest_version">Latest version</s:String>
|
||||
<s:String x:Key="up_release_notes">Release notes</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Release notes are unavailable for this release.</s:String>
|
||||
<s:String x:Key="up_show_more">Show full changelog</s:String>
|
||||
<s:String x:Key="up_show_less">Show latest notes</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Loading changelog...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Failed to load the full changelog. The latest notes are shown instead.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Versión actual</s:String>
|
||||
<s:String x:Key="up_latest_version">Última versión</s:String>
|
||||
<s:String x:Key="up_release_notes">Notas de la versión</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Las notas de la versión no están disponibles para esta versión.</s:String>
|
||||
<s:String x:Key="up_show_more">Mostrar changelog completo</s:String>
|
||||
<s:String x:Key="up_show_less">Mostrar solo las notas actuales</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Cargando changelog...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">No se pudo cargar el changelog completo. Se muestran las notas actuales.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Version actuelle</s:String>
|
||||
<s:String x:Key="up_latest_version">Dernière version</s:String>
|
||||
<s:String x:Key="up_release_notes">Notes de version</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Les notes de version ne sont pas disponibles pour cette version.</s:String>
|
||||
<s:String x:Key="up_show_more">Afficher le changelog complet</s:String>
|
||||
<s:String x:Key="up_show_less">Afficher uniquement les notes actuelles</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Chargement du changelog...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Impossible de charger le changelog complet. Les notes actuelles sont affichées à la place.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Versione corrente</s:String>
|
||||
<s:String x:Key="up_latest_version">Ultima versione</s:String>
|
||||
<s:String x:Key="up_release_notes">Note di rilascio</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Le note di rilascio non sono disponibili per questa versione.</s:String>
|
||||
<s:String x:Key="up_show_more">Mostra il changelog completo</s:String>
|
||||
<s:String x:Key="up_show_less">Mostra solo le note correnti</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Caricamento del changelog...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Impossibile caricare il changelog completo. Vengono mostrate solo le note correnti.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">現在のバージョン</s:String>
|
||||
<s:String x:Key="up_latest_version">最新バージョン</s:String>
|
||||
<s:String x:Key="up_release_notes">リリースノート</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">このリリースのリリースノートは利用できません。</s:String>
|
||||
<s:String x:Key="up_show_more">完全な変更履歴を表示</s:String>
|
||||
<s:String x:Key="up_show_less">最新のリリースノートのみ表示</s:String>
|
||||
<s:String x:Key="up_loading_changelog">変更履歴を読み込み中...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">完全な変更履歴を読み込めませんでした。代わりに最新のリリースノートを表示しています。</s:String>
|
||||
<s:String x:Key="up_update_now">今すぐ更新</s:String>
|
||||
<s:String x:Key="up_popup_title">アップデート利用可能!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Aktualna wersja</s:String>
|
||||
<s:String x:Key="up_latest_version">Najnowsza wersja</s:String>
|
||||
<s:String x:Key="up_release_notes">Informacje o wydaniu</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Informacje o wydaniu są niedostępne dla tej wersji.</s:String>
|
||||
<s:String x:Key="up_show_more">Pokaż cały changelog</s:String>
|
||||
<s:String x:Key="up_show_less">Pokaż tylko bieżące zmiany</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Ładowanie changeloga...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Nie udało się załadować pełnego changeloga. Zamiast tego wyświetlono bieżące zmiany.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Versão atual</s:String>
|
||||
<s:String x:Key="up_latest_version">Versão mais recente</s:String>
|
||||
<s:String x:Key="up_release_notes">Notas da versão</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">As notas da versão não estão disponíveis para esta versão.</s:String>
|
||||
<s:String x:Key="up_show_more">Mostrar changelog completo</s:String>
|
||||
<s:String x:Key="up_show_less">Mostrar apenas as notas atuais</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Carregando changelog...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Falha ao carregar o changelog completo. As notas atuais estão sendo exibidas.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Текущая версия</s:String>
|
||||
<s:String x:Key="up_latest_version">Новая версия</s:String>
|
||||
<s:String x:Key="up_release_notes">Что нового</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Для этого релиза патчноуты недоступны.</s:String>
|
||||
<s:String x:Key="up_show_more">Показать весь changelog</s:String>
|
||||
<s:String x:Key="up_show_less">Показать только актуальные изменения</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Загрузка changelog...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Не удалось загрузить полный changelog. Показаны только актуальные изменения.</s:String>
|
||||
<s:String x:Key="up_update_now">Обновить сейчас</s:String>
|
||||
<s:String x:Key="up_popup_title">Доступно обновление!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Geçerli sürüm</s:String>
|
||||
<s:String x:Key="up_latest_version">En son sürüm</s:String>
|
||||
<s:String x:Key="up_release_notes">Sürüm notları</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Bu sürüm için sürüm notları kullanılamıyor.</s:String>
|
||||
<s:String x:Key="up_show_more">Tüm changelog'u göster</s:String>
|
||||
<s:String x:Key="up_show_less">Yalnızca güncel notları göster</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Changelog yükleniyor...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Tam changelog yüklenemedi. Bunun yerine güncel notlar gösteriliyor.</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>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">Поточна версія</s:String>
|
||||
<s:String x:Key="up_latest_version">Остання версія</s:String>
|
||||
<s:String x:Key="up_release_notes">Нотатки до релізу</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">Нотатки до цього релізу недоступні.</s:String>
|
||||
<s:String x:Key="up_show_more">Показати весь список змін</s:String>
|
||||
<s:String x:Key="up_show_less">Показати лише актуальні зміни</s:String>
|
||||
<s:String x:Key="up_loading_changelog">Завантаження списку змін...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">Не вдалося завантажити повний список змін. Натомість показано лише актуальні зміни.</s:String>
|
||||
<s:String x:Key="up_update_now">Оновити зараз</s:String>
|
||||
<s:String x:Key="up_popup_title">Доступне оновлення!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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_current_version">当前版本</s:String>
|
||||
<s:String x:Key="up_latest_version">最新版本</s:String>
|
||||
<s:String x:Key="up_release_notes">更新说明</s:String>
|
||||
<s:String x:Key="up_release_notes_unavailable">此版本的更新说明不可用。</s:String>
|
||||
<s:String x:Key="up_show_more">显示完整更新日志</s:String>
|
||||
<s:String x:Key="up_show_less">仅显示当前说明</s:String>
|
||||
<s:String x:Key="up_loading_changelog">正在加载更新日志...</s:String>
|
||||
<s:String x:Key="up_changelog_failed">无法加载完整更新日志。当前仅显示本次说明。</s:String>
|
||||
<s:String x:Key="up_update_now">立即更新</s:String>
|
||||
<s:String x:Key="up_popup_title">有更新可用!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.1.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.9.1")]
|
||||
[assembly: AssemblyFileVersion("1.0.9.1")]
|
||||
Generated
+2
-2
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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
-1
@@ -1,7 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace WeModPatcher.ReactiveUICore
|
||||
namespace WandEnhancer.ReactiveUICore
|
||||
{
|
||||
public class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
+1
-1
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using WandEnhancer.Models;
|
||||
|
||||
namespace WandEnhancer.Utils
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static WeModConfig CheckWeModPath(string versionRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
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
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static WeModConfig FindWeMod()
|
||||
{
|
||||
string localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
|
||||
if (!string.IsNullOrEmpty(localAppDataPath))
|
||||
{
|
||||
foreach (var folder in Constants.WeModBrandNames)
|
||||
{
|
||||
var weModDir = Path.Combine(localAppDataPath, folder);
|
||||
if (!Directory.Exists(weModDir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep scanning the other brand folders if this one has no valid
|
||||
// install instead of giving up on the first folder that exists.
|
||||
var config = FindLatestWeMod(weModDir);
|
||||
if (config != null)
|
||||
{
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: a running Wand/WeMod process reveals the install directory
|
||||
// wherever it lives (non-default LOCALAPPDATA, moved install, other drive).
|
||||
return FindWeModFromRunningProcess();
|
||||
}
|
||||
|
||||
private static WeModConfig FindWeModFromRunningProcess()
|
||||
{
|
||||
foreach (var name in Constants.WeModBrandNames)
|
||||
{
|
||||
Process[] processes;
|
||||
try
|
||||
{
|
||||
processes = Process.GetProcessesByName(name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var process in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exePath = process.MainModule?.FileName;
|
||||
if (string.IsNullOrEmpty(exePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process may be the versioned exe (dir is the install root) or
|
||||
// the launcher stub at the parent (dir holds `app-*` subfolders).
|
||||
var processDir = Path.GetDirectoryName(exePath);
|
||||
var config = CheckWeModPath(processDir) ?? FindLatestWeMod(processDir);
|
||||
if (config != null)
|
||||
{
|
||||
return config;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// MainModule throws on access-denied / bitness mismatch; skip.
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string Base64Decode(string base64EncodedData)
|
||||
{
|
||||
var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
|
||||
return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
|
||||
}
|
||||
|
||||
public static string Base64Encode(string plainText)
|
||||
{
|
||||
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
||||
return System.Convert.ToBase64String(plainTextBytes);
|
||||
}
|
||||
|
||||
public static WeModConfig FindLatestWeMod(string root)
|
||||
{
|
||||
var appFolders = Directory.EnumerateDirectories(root)
|
||||
.Select(folderPath => new DirectoryInfo(folderPath))
|
||||
.Where(dirInfo => Regex.IsMatch(dirInfo.Name, @"^app-\w+"))
|
||||
.Select(dirInfo => new
|
||||
{
|
||||
Name = dirInfo.Name,
|
||||
Path = dirInfo.FullName,
|
||||
LastModified = dirInfo.LastWriteTime
|
||||
})
|
||||
.OrderByDescending(item => item.LastModified)
|
||||
.ToList();
|
||||
|
||||
|
||||
return appFolders
|
||||
.Select(folder => CheckWeModPath(folder.Path))
|
||||
.FirstOrDefault(config => config != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace WandEnhancer.Utils
|
||||
{
|
||||
public class UpdateReleaseInfo
|
||||
{
|
||||
public string Version { get; set; }
|
||||
|
||||
public string LatestNotes { get; set; }
|
||||
}
|
||||
|
||||
public class GitHubRelease
|
||||
{
|
||||
public class AssetsType
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("browser_download_url")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty("tag_name")]
|
||||
public string TagName { get; set; }
|
||||
|
||||
[JsonProperty("assets")]
|
||||
public AssetsType[] Assets { get; set; }
|
||||
|
||||
[JsonProperty("body")]
|
||||
public string Body { get; set; }
|
||||
|
||||
[JsonProperty("published_at")]
|
||||
public DateTimeOffset PublishedAt { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class Updater
|
||||
{
|
||||
private GitHubRelease _release = null;
|
||||
private UpdateReleaseInfo _updateInfo = null;
|
||||
private string _fullChangelog = null;
|
||||
private static readonly HttpClient _httpClient = new HttpClient()
|
||||
{
|
||||
DefaultRequestHeaders =
|
||||
{
|
||||
{ "User-Agent", "GitHub-Updater" }
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly string ApiUrl = $"https://api.github.com/repos/{Constants.Owner}/{Constants.RepoName}/releases/latest";
|
||||
private static readonly string ReleasesApiUrl = $"https://api.github.com/repos/{Constants.Owner}/{Constants.RepoName}/releases?per_page=20";
|
||||
public async Task<bool> CheckForUpdates()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
var response = await _httpClient.GetAsync(ApiUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
_release = JsonConvert.DeserializeObject<GitHubRelease>(await response.Content.ReadAsStringAsync());
|
||||
_updateInfo = null;
|
||||
_fullChangelog = null;
|
||||
|
||||
if (_release == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var latestVersion = ParseVersion(_release.TagName);
|
||||
|
||||
if (latestVersion <= currentVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_updateInfo = new UpdateReleaseInfo
|
||||
{
|
||||
Version = NormalizeVersion(_release.TagName),
|
||||
LatestNotes = NormalizeText(_release.Body)
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UpdateReleaseInfo> GetUpdateInfoAsync()
|
||||
{
|
||||
if (_updateInfo != null)
|
||||
{
|
||||
return _updateInfo;
|
||||
}
|
||||
|
||||
return await CheckForUpdates()
|
||||
? _updateInfo
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task<string> GetFullChangelogAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_fullChangelog))
|
||||
{
|
||||
return NormalizeText(_fullChangelog);
|
||||
}
|
||||
|
||||
_fullChangelog = await TryLoadFullChangelogAsync();
|
||||
|
||||
return NormalizeText(_fullChangelog);
|
||||
}
|
||||
|
||||
public async Task Update()
|
||||
{
|
||||
if (_release == null)
|
||||
{
|
||||
throw new Exception("No release found");
|
||||
}
|
||||
|
||||
var asset = _release.Assets.FirstOrDefault(o => o.Name.EndsWith(".exe"));
|
||||
if(asset == null)
|
||||
{
|
||||
throw new Exception("No asset found");
|
||||
}
|
||||
|
||||
// download to temp
|
||||
var downloadPath = Path.Combine(Path.GetTempPath(), asset.Name);
|
||||
|
||||
using(var response = await _httpClient.GetAsync(asset.Url))
|
||||
using(var fileStream = File.Create(downloadPath))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
}
|
||||
|
||||
ApplyUpdate(downloadPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void ApplyUpdate(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentExecutable = Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
var psCommand = $"Start-Sleep -Seconds 2; " +
|
||||
$"Copy-Item -Path '{filePath}' -Destination '{currentExecutable}' -Force; " +
|
||||
$"Remove-Item -Path '{filePath}' -Force; " +
|
||||
$"Start-Sleep -Seconds 1; " +
|
||||
$"Start-Process -FilePath '{currentExecutable}';";
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = $"-WindowStyle Hidden -ExecutionPolicy Bypass -Command \"{psCommand}\"",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
|
||||
Task.Delay(500).ContinueWith(_ =>
|
||||
{
|
||||
App.Shutdown();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Update failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Version ParseVersion(string versionTag)
|
||||
{
|
||||
return new Version(NormalizeVersion(versionTag));
|
||||
}
|
||||
|
||||
private static string NormalizeVersion(string versionTag)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(versionTag))
|
||||
{
|
||||
throw new ArgumentException("Version tag cannot be empty.", nameof(versionTag));
|
||||
}
|
||||
|
||||
return versionTag.Trim().TrimStart('v', 'V');
|
||||
}
|
||||
|
||||
private static string NormalizeText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return NormalizeLineEndings(text).Trim();
|
||||
}
|
||||
|
||||
private static string NormalizeLineEndings(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\r\n", "\n")
|
||||
.Replace('\r', '\n');
|
||||
}
|
||||
|
||||
private static async Task<string> TryLoadFullChangelogAsync()
|
||||
{
|
||||
return await TryBuildReleaseHistoryAsync();
|
||||
}
|
||||
|
||||
private static async Task<string> TryBuildReleaseHistoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(ReleasesApiUrl);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var releases = JsonConvert.DeserializeObject<GitHubRelease[]>(await response.Content.ReadAsStringAsync());
|
||||
if (releases == null || releases.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildReleaseHistory(releases);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildReleaseHistory(GitHubRelease[] releases)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
foreach (var release in releases.Where(item => !string.IsNullOrWhiteSpace(item?.TagName)))
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.Append("## [")
|
||||
.Append(NormalizeVersion(release.TagName))
|
||||
.Append("]");
|
||||
|
||||
if (release.PublishedAt != default(DateTimeOffset))
|
||||
{
|
||||
builder.Append(" - ")
|
||||
.Append(release.PublishedAt.UtcDateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
var notes = NormalizeText(release.Body);
|
||||
if (string.IsNullOrWhiteSpace(notes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine();
|
||||
builder.Append(notes);
|
||||
}
|
||||
|
||||
return NormalizeText(builder.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
+2
-2
@@ -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"
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
+38
-15
@@ -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>
|
||||
+1
-1
@@ -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
|
||||
+144
-67
@@ -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,35 +185,117 @@ namespace WeModPatcher.View.MainWindow
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnUpdate(object param)
|
||||
private async void OnUpdate(object param)
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
var updateInfo = await _updater.GetUpdateInfoAsync();
|
||||
if (updateInfo == null)
|
||||
{
|
||||
Log("No update details are available right now.", ELogType.Warn);
|
||||
return;
|
||||
}
|
||||
|
||||
MainWindow.Instance.OpenPopup(new UpdatePopup(Constants.Version.ToString(), updateInfo.Version,
|
||||
updateInfo.LatestNotes, () =>
|
||||
{
|
||||
MainWindow.Instance.ClosePopup();
|
||||
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);
|
||||
});
|
||||
}, () => _updater.GetFullChangelogAsync()), 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());
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var isUpdateAvailable = await _updater.CheckForUpdates();
|
||||
Application.Current.Dispatcher.Invoke(() => IsUpdateAvailable = isUpdateAvailable);
|
||||
});
|
||||
_view = view;
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="Auto" d:DesignWidth="Auto"
|
||||
Background="{DynamicResource Background}"
|
||||
Foreground="{DynamicResource MutedForeground}"
|
||||
FontWeight="Medium"
|
||||
FontSize="13">
|
||||
<Grid MinWidth="500" MaxWidth="620">
|
||||
<Grid.Resources>
|
||||
<Style x:Key="UpdateActionButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="{DynamicResource Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Primary}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Primary}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Primary}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" HorizontalAlignment="Left" MaxWidth="560"
|
||||
Padding="9 4" CornerRadius="4"
|
||||
Background="{DynamicResource Muted}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Ellipse Width="7" Height="7" Fill="{DynamicResource Destructive}"
|
||||
Margin="0 0 8 0" VerticalAlignment="Center" />
|
||||
<TextBlock Foreground="{DynamicResource Foreground}" FontSize="11.5"
|
||||
Text="{DynamicResource up_warning}" TextWrapping="Wrap" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<WrapPanel Grid.Row="1" Margin="0 10 0 0" Orientation="Horizontal">
|
||||
<Border Padding="8 4" Margin="0 0 8 0" CornerRadius="4"
|
||||
Background="{DynamicResource Muted}" BorderBrush="{DynamicResource Border}" BorderThickness="1">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0 0 7 0" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource MutedForeground}" FontSize="10.5"
|
||||
Text="{DynamicResource up_current_version}" />
|
||||
<TextBlock x:Name="CurrentVersionValue" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource Foreground}" FontSize="12.5" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Padding="8 4" CornerRadius="4"
|
||||
Background="{DynamicResource Primary}" BorderThickness="1">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0 0 7 0" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryForeground}" FontSize="10.5"
|
||||
Text="{DynamicResource up_latest_version}" />
|
||||
<TextBlock x:Name="LatestVersionValue" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryForeground}" FontSize="12.5" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</WrapPanel>
|
||||
|
||||
<TextBlock Grid.Row="2" Margin="0 12 0 6" Foreground="{DynamicResource Foreground}"
|
||||
FontSize="14" FontWeight="Bold" Text="{DynamicResource up_release_notes}" />
|
||||
|
||||
<Border Grid.Row="3"
|
||||
Background="{DynamicResource Muted}" BorderBrush="{DynamicResource Border}" BorderThickness="1" CornerRadius="4">
|
||||
<ScrollViewer x:Name="NotesScrollViewer"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
CanContentScroll="False">
|
||||
<TextBox x:Name="NotesTextBlock" Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
Padding="12"
|
||||
BorderThickness="0" IsReadOnly="True"
|
||||
Foreground="{DynamicResource Foreground}"
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="4" Margin="0 12 0 0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button x:Name="ShowMoreButton" Grid.Column="1" Margin="0 0 10 0"
|
||||
Padding="12 5" Content="{DynamicResource up_show_more}"
|
||||
Click="OnShowMoreClick" />
|
||||
|
||||
<Button Grid.Column="2" Padding="18 5" Style="{StaticResource UpdateActionButton}"
|
||||
Content="{DynamicResource up_update_now}"
|
||||
Click="OnUpdateClick" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WandEnhancer.View.Popups
|
||||
{
|
||||
public partial class UpdatePopup : UserControl
|
||||
{
|
||||
private readonly Action _onUpdate;
|
||||
private readonly Func<Task<string>> _loadFullChangelog;
|
||||
private readonly string _latestNotes;
|
||||
private string _fullChangelog;
|
||||
private bool _showingFullChangelog;
|
||||
|
||||
public UpdatePopup(string currentVersion, string latestVersion, string latestNotes, Action onUpdate,
|
||||
Func<Task<string>> loadFullChangelog)
|
||||
{
|
||||
_onUpdate = onUpdate;
|
||||
_loadFullChangelog = loadFullChangelog;
|
||||
InitializeComponent();
|
||||
|
||||
CurrentVersionValue.Text = currentVersion;
|
||||
LatestVersionValue.Text = latestVersion;
|
||||
_latestNotes = string.IsNullOrWhiteSpace(latestNotes)
|
||||
? GetResourceText("up_release_notes_unavailable")
|
||||
: latestNotes;
|
||||
|
||||
SetNotesText(_latestNotes);
|
||||
ShowMoreButton.Visibility = loadFullChangelog == null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
private void OnUpdateClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_onUpdate();
|
||||
}
|
||||
|
||||
private async void OnShowMoreClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_loadFullChangelog == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_showingFullChangelog)
|
||||
{
|
||||
SetNotesText(_latestNotes);
|
||||
ShowMoreButton.Content = GetResourceText("up_show_more");
|
||||
_showingFullChangelog = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_fullChangelog))
|
||||
{
|
||||
ShowMoreButton.IsEnabled = false;
|
||||
ShowMoreButton.Content = GetResourceText("up_loading_changelog");
|
||||
|
||||
try
|
||||
{
|
||||
_fullChangelog = await _loadFullChangelog();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ShowMoreButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_fullChangelog))
|
||||
{
|
||||
ShowMoreButton.Content = GetResourceText("up_show_more");
|
||||
SetNotesText(string.Concat(
|
||||
_latestNotes,
|
||||
Environment.NewLine,
|
||||
Environment.NewLine,
|
||||
GetResourceText("up_changelog_failed")));
|
||||
return;
|
||||
}
|
||||
|
||||
SetNotesText(_fullChangelog);
|
||||
ShowMoreButton.Content = GetResourceText("up_show_less");
|
||||
_showingFullChangelog = true;
|
||||
}
|
||||
|
||||
private void SetNotesText(string text)
|
||||
{
|
||||
NotesTextBlock.Text = text ?? string.Empty;
|
||||
NotesScrollViewer.ScrollToTop();
|
||||
}
|
||||
|
||||
private static string GetResourceText(string key)
|
||||
{
|
||||
return Application.Current.TryFindResource(key) as string ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,13 @@
|
||||
<Prefer32bit>false</Prefer32bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject>WeModPatcher.Program</StartupObject>
|
||||
<StartupObject>WandEnhancer.Program</StartupObject>
|
||||
<CMakeSourceDir>..\tools\asar-fuses-bypass</CMakeSourceDir>
|
||||
<NativeBuildRoot>..\.tmp\cmake</NativeBuildRoot>
|
||||
<NativeBuildConfiguration Condition="'$(Configuration)' == 'Debug'">Debug</NativeBuildConfiguration>
|
||||
<NativeBuildConfiguration Condition="'$(NativeBuildConfiguration)' == ''">Release</NativeBuildConfiguration>
|
||||
<CMakeBuildDir>$(NativeBuildRoot)\asar-fuses-bypass</CMakeBuildDir>
|
||||
<ProxyDllPath>$(CMakeBuildDir)\$(NativeBuildConfiguration)\version.dll</ProxyDllPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
@@ -65,18 +71,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 +96,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 +125,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">
|
||||
@@ -126,10 +154,21 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AsarSharp\AsarSharp.csproj">
|
||||
<Project>{beaa604a-402a-4387-8903-a53fc913a26e}</Project>
|
||||
<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>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
@@ -138,6 +177,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="ValidateNativeArtifacts" 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>
|
||||
@@ -145,13 +192,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyList Include="$(OutputPath)*.dll" />
|
||||
<AssemblyList Include="$(OutputPath)*.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DllList>@(AssemblyList->'%(FullPath)', ' ')</DllList>
|
||||
</PropertyGroup>
|
||||
|
||||
<Delete Files="$(OutputPath)$(AssemblyName).pdb" ContinueOnError="true" />
|
||||
<Exec Command=""$(ILRepackExe)" /allowMultiple /copyattrs /out:"$(OutputPath)$(AssemblyName).exe" "$(MainAssembly)" $(DllList)" />
|
||||
<Delete Files="@(AssemblyList)" ContinueOnError="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,164 +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 List<uint>();
|
||||
while (Imports.WaitForDebugEvent(ref debugEvent, uint.MaxValue))
|
||||
{
|
||||
var code = debugEvent.dwDebugEventCode;
|
||||
if (code == Imports.CREATE_PROCESS_DEBUG_EVENT)
|
||||
{
|
||||
processIds.Add(debugEvent.dwProcessId);
|
||||
}
|
||||
else if (code == Imports.EXIT_PROCESS_DEBUG_EVENT)
|
||||
{
|
||||
processIds.Remove(debugEvent.dwProcessId);
|
||||
|
||||
if(processIds.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (code == Imports.EXCEPTION_DEBUG_EVENT)
|
||||
{
|
||||
var exceptionInfo = Imports.MapUnmanagedStructure<Imports.EXCEPTION_DEBUG_INFO>(debugEvent.Union);
|
||||
|
||||
if (exceptionInfo.ExceptionRecord.ExceptionCode == Imports.EXCEPTION_BREAKPOINT)
|
||||
{
|
||||
var process = Process.GetProcessById((int)debugEvent.dwProcessId);
|
||||
var address = MemoryUtils.ScanVirtualMemory(
|
||||
process.Handle,
|
||||
process.Modules[0].BaseAddress,
|
||||
process.Modules[0].ModuleMemorySize,
|
||||
Constants.ExePatchSignature.Sequence, Constants.ExePatchSignature.Mask
|
||||
);
|
||||
|
||||
if (address != IntPtr.Zero)
|
||||
{
|
||||
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, Imports.DBG_CONTINUE);
|
||||
}
|
||||
|
||||
foreach (var processId in processIds)
|
||||
{
|
||||
Imports.DebugActiveProcessStop(processId);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WeModPatcher.Utils
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static bool CheckWeModPath(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(Path.Combine(root, "WeMod.exe")) &&
|
||||
File.Exists(Path.Combine(root, "resources", "app.asar"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static string FindWeModDirectory()
|
||||
{
|
||||
string localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
|
||||
string defaultDir = Path.Combine(localAppDataPath ?? "", "WeMod");
|
||||
|
||||
if (!Directory.Exists(defaultDir))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindLatestWeMod(defaultDir);
|
||||
}
|
||||
|
||||
public static string Base64Decode(string base64EncodedData)
|
||||
{
|
||||
var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
|
||||
return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
|
||||
}
|
||||
|
||||
public static string Base64Encode(string plainText)
|
||||
{
|
||||
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
||||
return System.Convert.ToBase64String(plainTextBytes);
|
||||
}
|
||||
|
||||
public static string FindLatestWeMod(string root)
|
||||
{
|
||||
var appFolders = Directory.EnumerateDirectories(root)
|
||||
.Select(folderPath => new DirectoryInfo(folderPath))
|
||||
.Where(dirInfo => Regex.IsMatch(dirInfo.Name, @"^app-\w+"))
|
||||
.Select(dirInfo => new
|
||||
{
|
||||
Name = dirInfo.Name,
|
||||
Path = dirInfo.FullName,
|
||||
LastModified = dirInfo.LastWriteTime
|
||||
})
|
||||
.OrderByDescending(item => item.LastModified)
|
||||
.ToList();
|
||||
|
||||
return (
|
||||
from folder
|
||||
in appFolders
|
||||
where CheckWeModPath(folder.Path)
|
||||
select folder.Path
|
||||
).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.Http;
|
||||
using System.Windows;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace WeModPatcher.Utils
|
||||
{
|
||||
public class GitHubRelease
|
||||
{
|
||||
public class AssetsType
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("browser_download_url")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty("tag_name")]
|
||||
public string TagName { get; set; }
|
||||
|
||||
[JsonProperty("assets")]
|
||||
public AssetsType[] Assets { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class Updater
|
||||
{
|
||||
private GitHubRelease _release = null;
|
||||
private static readonly HttpClient _httpClient = new HttpClient()
|
||||
{
|
||||
DefaultRequestHeaders =
|
||||
{
|
||||
{ "User-Agent", "GitHub-Updater" }
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly string ApiUrl = $"https://api.github.com/repos/{Constants.Owner}/{Constants.RepoName}/releases/latest";
|
||||
public async Task<bool> CheckForUpdates()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentVersion = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
var response = await _httpClient.GetAsync(ApiUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
_release = JsonConvert.DeserializeObject<GitHubRelease>(await response.Content.ReadAsStringAsync());
|
||||
|
||||
if (_release == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var latestVersion = new Version(_release.TagName);
|
||||
|
||||
return latestVersion > currentVersion;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Update()
|
||||
{
|
||||
if (_release == null)
|
||||
{
|
||||
throw new Exception("No release found");
|
||||
}
|
||||
|
||||
var asset = _release.Assets.FirstOrDefault(o => o.Name.EndsWith(".exe"));
|
||||
if(asset == null)
|
||||
{
|
||||
throw new Exception("No asset found");
|
||||
}
|
||||
|
||||
// download to temp
|
||||
var downloadPath = Path.Combine(Path.GetTempPath(), asset.Name);
|
||||
|
||||
using(var response = await _httpClient.GetAsync(asset.Url))
|
||||
using(var fileStream = File.Create(downloadPath))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
}
|
||||
|
||||
ApplyUpdate(downloadPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void ApplyUpdate(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentExecutable = Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
var psCommand = $"Start-Sleep -Seconds 2; " +
|
||||
$"Copy-Item -Path '{filePath}' -Destination '{currentExecutable}' -Force; " +
|
||||
$"Remove-Item -Path '{filePath}' -Force; " +
|
||||
$"Start-Sleep -Seconds 1; " +
|
||||
$"Start-Process -FilePath '{currentExecutable}';";
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
Arguments = $"-WindowStyle Hidden -ExecutionPolicy Bypass -Command \"{psCommand}\"",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
|
||||
Task.Delay(500).ContinueWith(_ =>
|
||||
{
|
||||
App.Shutdown();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Update failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,259 +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;
|
||||
|
||||
// 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: 177 KiB |
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0build.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
@@ -0,0 +1,155 @@
|
||||
param(
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$webPanelDir = Join-Path $repoRoot 'web-panel'
|
||||
$nativeBuildRoot = Join-Path $repoRoot '.tmp/cmake'
|
||||
$asarFusesSourceDir = Join-Path $repoRoot 'tools/asar-fuses-bypass'
|
||||
$asarFusesBuildDir = Join-Path $nativeBuildRoot 'asar-fuses-bypass'
|
||||
$solutionPath = Join-Path $repoRoot 'Wand-Enhancer.sln'
|
||||
|
||||
function Resolve-CommandPath {
|
||||
param([string]$Name)
|
||||
|
||||
$command = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if (-not $command) {
|
||||
throw "Required command not found in PATH: $Name"
|
||||
}
|
||||
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
function Resolve-NuGetPath {
|
||||
$nugetCommand = Get-Command 'nuget.exe' -ErrorAction SilentlyContinue
|
||||
if (-not $nugetCommand) {
|
||||
$nugetCommand = Get-Command 'nuget' -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if ($nugetCommand) {
|
||||
return $nugetCommand.Source
|
||||
}
|
||||
|
||||
$toolsDir = Join-Path $repoRoot '.tmp/tools'
|
||||
$nugetPath = Join-Path $toolsDir 'nuget.exe'
|
||||
if (-not (Test-Path $nugetPath)) {
|
||||
New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null
|
||||
Invoke-WebRequest -Uri 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile $nugetPath
|
||||
}
|
||||
|
||||
return $nugetPath
|
||||
}
|
||||
|
||||
function Resolve-MSBuildPath {
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
throw "vswhere.exe not found: $vswhere"
|
||||
}
|
||||
|
||||
# No version pin: pick whatever VS the host has (2022/2026/newer) so CI
|
||||
# keeps working when the runner image bumps its Visual Studio major.
|
||||
$installationPath = & $vswhere -latest -prerelease -products '*' -requires Microsoft.Component.MSBuild -property installationPath
|
||||
if ([string]::IsNullOrWhiteSpace($installationPath)) {
|
||||
throw 'Visual Studio with MSBuild was not found.'
|
||||
}
|
||||
|
||||
$msbuildPath = Join-Path $installationPath 'MSBuild\Current\Bin\MSBuild.exe'
|
||||
if (-not (Test-Path $msbuildPath)) {
|
||||
throw "MSBuild.exe not found: $msbuildPath"
|
||||
}
|
||||
|
||||
return $msbuildPath
|
||||
}
|
||||
|
||||
function Invoke-Step {
|
||||
param(
|
||||
[string]$Label,
|
||||
[scriptblock]$Action
|
||||
)
|
||||
|
||||
Write-Host "==> $Label" -ForegroundColor Cyan
|
||||
& $Action
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Step failed: $Label"
|
||||
}
|
||||
}
|
||||
|
||||
$cmake = Resolve-CommandPath 'cmake'
|
||||
$nuget = Resolve-NuGetPath
|
||||
$pnpm = Resolve-CommandPath 'pnpm'
|
||||
$msbuild = Resolve-MSBuildPath
|
||||
|
||||
Invoke-Step 'Install web-panel dependencies' {
|
||||
& $pnpm --dir $webPanelDir install --frozen-lockfile
|
||||
}
|
||||
|
||||
Invoke-Step 'Build web-panel' {
|
||||
& $pnpm --dir $webPanelDir run build
|
||||
}
|
||||
|
||||
Invoke-Step 'Configure asar-fuses-bypass' {
|
||||
# Let CMake choose its default Visual Studio generator (matches the host VS),
|
||||
# avoiding a hardcoded/derived name that breaks when the runner bumps VS.
|
||||
# Clearing CMAKE_GENERATOR ensures the default isn't overridden to a non-VS
|
||||
# generator that would reject the -A architecture flag.
|
||||
Remove-Item Env:CMAKE_GENERATOR -ErrorAction SilentlyContinue
|
||||
& $cmake -S $asarFusesSourceDir -B $asarFusesBuildDir -A x64
|
||||
}
|
||||
|
||||
Invoke-Step 'Build asar-fuses-bypass' {
|
||||
& $cmake --build $asarFusesBuildDir --config $Configuration
|
||||
}
|
||||
|
||||
Invoke-Step 'Restore NuGet packages' {
|
||||
& $nuget restore $solutionPath -NonInteractive
|
||||
}
|
||||
|
||||
Invoke-Step 'Build solution' {
|
||||
& $msbuild $solutionPath /m /p:Configuration=$Configuration '/p:Platform=Any CPU' /t:Build
|
||||
}
|
||||
|
||||
# Code-sign the release executable. A self-signed signature notably lowers
|
||||
# false-positive AV/VirusTotal detections. The cert is reused across builds and
|
||||
# generated on first use, so no secrets or env configuration are required.
|
||||
if ($Configuration -eq 'Release') {
|
||||
Write-Host '==> Sign WandEnhancer.exe' -ForegroundColor Cyan
|
||||
|
||||
$exePath = Join-Path $repoRoot "WandEnhancer/bin/$Configuration/WandEnhancer.exe"
|
||||
if (-not (Test-Path $exePath)) {
|
||||
throw "Executable not found for signing: $exePath"
|
||||
}
|
||||
|
||||
$signingSubject = 'CN=Wand-Enhancer'
|
||||
$cert = Get-ChildItem Cert:\CurrentUser\My |
|
||||
Where-Object { $_.Subject -eq $signingSubject -and $_.HasPrivateKey } |
|
||||
Select-Object -First 1
|
||||
if (-not $cert) {
|
||||
$cert = New-SelfSignedCertificate `
|
||||
-Subject $signingSubject `
|
||||
-Type CodeSigningCert `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-KeyExportPolicy Exportable `
|
||||
-KeyUsage DigitalSignature `
|
||||
-KeyAlgorithm RSA `
|
||||
-KeyLength 2048 `
|
||||
-HashAlgorithm SHA256 `
|
||||
-NotAfter (Get-Date).AddYears(5)
|
||||
Write-Host "Generated self-signed code-signing certificate: $($cert.Subject) [$($cert.Thumbprint)]"
|
||||
}
|
||||
|
||||
$signature = Set-AuthenticodeSignature -FilePath $exePath -Certificate $cert -HashAlgorithm SHA256
|
||||
# A self-signed root is intentionally untrusted, so the status is
|
||||
# 'UnknownError' (untrusted root) even though the signature is embedded.
|
||||
# Only a missing SignerCertificate means signing actually failed.
|
||||
if (-not $signature.SignerCertificate) {
|
||||
throw "Signing failed: $($signature.Status) - $($signature.StatusMessage)"
|
||||
}
|
||||
|
||||
Write-Host "Signed $exePath [$($cert.Thumbprint)] (status: $($signature.Status))"
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "Build completed successfully ($Configuration)." -ForegroundColor Green
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
script_path="$script_dir/build.ps1"
|
||||
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
script_path="$(cygpath -w "$script_path")"
|
||||
fi
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$script_path" "$@"
|
||||
@@ -0,0 +1,81 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version,
|
||||
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$changelogPath = Join-Path $repoRoot 'CHANGELOG.md'
|
||||
|
||||
function Normalize-Version {
|
||||
param([string]$Value)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||
throw 'Version value cannot be empty.'
|
||||
}
|
||||
|
||||
return $Value.Trim().TrimStart('v', 'V')
|
||||
}
|
||||
|
||||
function Get-ChangelogSection {
|
||||
param(
|
||||
[string]$Content,
|
||||
[string]$TargetVersion
|
||||
)
|
||||
|
||||
$normalizedTargetVersion = Normalize-Version $TargetVersion
|
||||
$normalizedContent = $Content -replace "`r`n", "`n" -replace "`r", "`n"
|
||||
$lines = $normalizedContent -split "`n"
|
||||
$builder = New-Object System.Text.StringBuilder
|
||||
$isInsideSection = $false
|
||||
|
||||
foreach ($line in $lines) {
|
||||
$match = [regex]::Match($line, '^##\s+\[(?<version>[^\]]+)\]')
|
||||
if ($match.Success) {
|
||||
if ($isInsideSection) {
|
||||
break
|
||||
}
|
||||
|
||||
$isInsideSection = (Normalize-Version $match.Groups['version'].Value) -eq $normalizedTargetVersion
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $isInsideSection) {
|
||||
continue
|
||||
}
|
||||
|
||||
[void]$builder.AppendLine($line)
|
||||
}
|
||||
|
||||
return $builder.ToString().Trim()
|
||||
}
|
||||
|
||||
if (-not (Test-Path $changelogPath)) {
|
||||
throw "CHANGELOG.md not found: $changelogPath"
|
||||
}
|
||||
|
||||
$changelogContent = Get-Content -Path $changelogPath -Raw
|
||||
$section = Get-ChangelogSection -Content $changelogContent -TargetVersion $Version
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($section)) {
|
||||
throw "Changelog section for version '$(Normalize-Version $Version)' was not found in CHANGELOG.md."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
Write-Output $section
|
||||
exit 0
|
||||
}
|
||||
|
||||
$resolvedOutputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) {
|
||||
$OutputPath
|
||||
}
|
||||
else {
|
||||
Join-Path $repoRoot $OutputPath
|
||||
}
|
||||
|
||||
Set-Content -Path $resolvedOutputPath -Value $section -Encoding utf8
|
||||
Write-Host "Wrote changelog section to $resolvedOutputPath"
|
||||
@@ -0,0 +1,111 @@
|
||||
param(
|
||||
[string]$ExpectedVersion
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$assemblyInfoPath = Join-Path $repoRoot 'WandEnhancer\Properties\AssemblyInfo.cs'
|
||||
$changelogPath = Join-Path $repoRoot 'CHANGELOG.md'
|
||||
|
||||
function Normalize-Version {
|
||||
param([string]$Value)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||
throw 'Version value cannot be empty.'
|
||||
}
|
||||
|
||||
return $Value.Trim().TrimStart('v', 'V')
|
||||
}
|
||||
|
||||
function Get-ChangelogSection {
|
||||
param(
|
||||
[string]$Content,
|
||||
[string]$TargetVersion
|
||||
)
|
||||
|
||||
$normalizedTargetVersion = Normalize-Version $TargetVersion
|
||||
$normalizedContent = $Content -replace "`r`n", "`n" -replace "`r", "`n"
|
||||
$lines = $normalizedContent -split "`n"
|
||||
$builder = New-Object System.Text.StringBuilder
|
||||
$isInsideSection = $false
|
||||
|
||||
foreach ($line in $lines) {
|
||||
$match = [regex]::Match($line, '^##\s+\[(?<version>[^\]]+)\]')
|
||||
if ($match.Success) {
|
||||
if ($isInsideSection) {
|
||||
break
|
||||
}
|
||||
|
||||
$isInsideSection = (Normalize-Version $match.Groups['version'].Value) -eq $normalizedTargetVersion
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $isInsideSection) {
|
||||
continue
|
||||
}
|
||||
|
||||
[void]$builder.AppendLine($line)
|
||||
}
|
||||
|
||||
return $builder.ToString().Trim()
|
||||
}
|
||||
|
||||
if (-not (Test-Path $assemblyInfoPath)) {
|
||||
throw "AssemblyInfo.cs not found: $assemblyInfoPath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $changelogPath)) {
|
||||
throw "CHANGELOG.md not found: $changelogPath"
|
||||
}
|
||||
|
||||
$assemblyInfoContent = Get-Content -Path $assemblyInfoPath -Raw
|
||||
$changelogContent = Get-Content -Path $changelogPath -Raw
|
||||
|
||||
$assemblyVersionMatch = [regex]::Match($assemblyInfoContent, '(?m)^\s*\[assembly:\s*AssemblyVersion\("(?<version>[^"]+)"\)\]')
|
||||
$fileVersionMatch = [regex]::Match($assemblyInfoContent, '(?m)^\s*\[assembly:\s*AssemblyFileVersion\("(?<version>[^"]+)"\)\]')
|
||||
|
||||
if (-not $assemblyVersionMatch.Success) {
|
||||
throw 'AssemblyVersion was not found in AssemblyInfo.cs.'
|
||||
}
|
||||
|
||||
if (-not $fileVersionMatch.Success) {
|
||||
throw 'AssemblyFileVersion was not found in AssemblyInfo.cs.'
|
||||
}
|
||||
|
||||
$assemblyVersion = Normalize-Version $assemblyVersionMatch.Groups['version'].Value
|
||||
$fileVersion = Normalize-Version $fileVersionMatch.Groups['version'].Value
|
||||
|
||||
if ($assemblyVersion -ne $fileVersion) {
|
||||
throw "AssemblyVersion ($assemblyVersion) and AssemblyFileVersion ($fileVersion) must match."
|
||||
}
|
||||
|
||||
$changelogVersionMatches = [regex]::Matches($changelogContent, '(?m)^##\s+\[(?<version>[^\]]+)\]')
|
||||
if ($changelogVersionMatches.Count -eq 0) {
|
||||
throw 'CHANGELOG.md must contain at least one version section.'
|
||||
}
|
||||
|
||||
$latestChangelogVersion = Normalize-Version $changelogVersionMatches[0].Groups['version'].Value
|
||||
if ($latestChangelogVersion -ne $assemblyVersion) {
|
||||
throw "The first CHANGELOG.md section ($latestChangelogVersion) must match AssemblyInfo.cs version ($assemblyVersion)."
|
||||
}
|
||||
|
||||
$latestSection = Get-ChangelogSection -Content $changelogContent -TargetVersion $assemblyVersion
|
||||
if ([string]::IsNullOrWhiteSpace($latestSection)) {
|
||||
throw "CHANGELOG.md section '$assemblyVersion' is empty."
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ExpectedVersion)) {
|
||||
$normalizedExpectedVersion = Normalize-Version $ExpectedVersion
|
||||
if ($normalizedExpectedVersion -ne $assemblyVersion) {
|
||||
throw "Release tag version ($normalizedExpectedVersion) must match AssemblyInfo.cs version ($assemblyVersion)."
|
||||
}
|
||||
|
||||
$expectedSection = Get-ChangelogSection -Content $changelogContent -TargetVersion $normalizedExpectedVersion
|
||||
if ([string]::IsNullOrWhiteSpace($expectedSection)) {
|
||||
throw "CHANGELOG.md section '$normalizedExpectedVersion' is missing or empty."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Validated release metadata for version $assemblyVersion"
|
||||
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
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 "")
|
||||
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_link_options(-static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
|
||||
add_library(version SHARED library.c library.def fuses.c)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// Created by kitbyte on 30.11.2025.
|
||||
//
|
||||
#include <Windows.h>
|
||||
#include <winver.h>
|
||||
|
||||
extern BOOL disable_asar_integrity(void);
|
||||
|
||||
static HMODULE g_originalVersionDll;
|
||||
|
||||
#define FOR_EACH_VERSION_FORWARDER(X) \
|
||||
X(GetFileVersionInfoA, BOOL, FALSE, \
|
||||
(LPCSTR filename, DWORD handle, DWORD length, LPVOID data), \
|
||||
(filename, handle, length, data)) \
|
||||
X(GetFileVersionInfoExA, BOOL, FALSE, \
|
||||
(DWORD flags, LPCSTR filename, DWORD handle, DWORD length, LPVOID data), \
|
||||
(flags, filename, handle, length, data)) \
|
||||
X(GetFileVersionInfoExW, BOOL, FALSE, \
|
||||
(DWORD flags, LPCWSTR filename, DWORD handle, DWORD length, LPVOID data), \
|
||||
(flags, filename, handle, length, data)) \
|
||||
X(GetFileVersionInfoSizeA, DWORD, 0, \
|
||||
(LPCSTR filename, LPDWORD handle), \
|
||||
(filename, handle)) \
|
||||
X(GetFileVersionInfoSizeExA, DWORD, 0, \
|
||||
(DWORD flags, LPCSTR filename, LPDWORD handle), \
|
||||
(flags, filename, handle)) \
|
||||
X(GetFileVersionInfoSizeExW, DWORD, 0, \
|
||||
(DWORD flags, LPCWSTR filename, LPDWORD handle), \
|
||||
(flags, filename, handle)) \
|
||||
X(GetFileVersionInfoSizeW, DWORD, 0, \
|
||||
(LPCWSTR filename, LPDWORD handle), \
|
||||
(filename, handle)) \
|
||||
X(GetFileVersionInfoW, BOOL, FALSE, \
|
||||
(LPCWSTR filename, DWORD handle, DWORD length, LPVOID data), \
|
||||
(filename, handle, length, data)) \
|
||||
X(VerFindFileA, DWORD, 0, \
|
||||
(DWORD flags, LPCSTR fileName, LPCSTR winDir, LPCSTR appDir, LPSTR curDir, PUINT curDirLen, LPSTR destDir, PUINT destDirLen), \
|
||||
(flags, fileName, winDir, appDir, curDir, curDirLen, destDir, destDirLen)) \
|
||||
X(VerFindFileW, DWORD, 0, \
|
||||
(DWORD flags, LPCWSTR fileName, LPCWSTR winDir, LPCWSTR appDir, LPWSTR curDir, PUINT curDirLen, LPWSTR destDir, PUINT destDirLen), \
|
||||
(flags, fileName, winDir, appDir, curDir, curDirLen, destDir, destDirLen)) \
|
||||
X(VerInstallFileA, DWORD, 0, \
|
||||
(DWORD flags, LPCSTR srcFileName, LPCSTR destFileName, LPCSTR srcDir, LPCSTR destDir, LPCSTR curDir, LPSTR tempFile, PUINT tempFileLen), \
|
||||
(flags, srcFileName, destFileName, srcDir, destDir, curDir, tempFile, tempFileLen)) \
|
||||
X(VerInstallFileW, DWORD, 0, \
|
||||
(DWORD flags, LPCWSTR srcFileName, LPCWSTR destFileName, LPCWSTR srcDir, LPCWSTR destDir, LPCWSTR curDir, LPWSTR tempFile, PUINT tempFileLen), \
|
||||
(flags, srcFileName, destFileName, srcDir, destDir, curDir, tempFile, tempFileLen)) \
|
||||
X(VerLanguageNameA, DWORD, 0, \
|
||||
(DWORD language, LPSTR buffer, DWORD bufferLength), \
|
||||
(language, buffer, bufferLength)) \
|
||||
X(VerLanguageNameW, DWORD, 0, \
|
||||
(DWORD language, LPWSTR buffer, DWORD bufferLength), \
|
||||
(language, buffer, bufferLength)) \
|
||||
X(VerQueryValueA, BOOL, FALSE, \
|
||||
(LPCVOID block, LPCSTR subBlock, LPVOID* buffer, PUINT bufferLength), \
|
||||
(block, subBlock, buffer, bufferLength)) \
|
||||
X(VerQueryValueW, BOOL, FALSE, \
|
||||
(LPCVOID block, LPCWSTR subBlock, LPVOID* buffer, PUINT bufferLength), \
|
||||
(block, subBlock, buffer, bufferLength))
|
||||
|
||||
#if defined(_MSC_VER) && !defined(_WIN64)
|
||||
|
||||
#define DECLARE_FORWARDER(name, return_type, default_value, params, args) \
|
||||
static FARPROC s_##name; \
|
||||
__declspec(naked) return_type WINAPI name params \
|
||||
{ \
|
||||
__asm \
|
||||
{ \
|
||||
jmp dword ptr [s_##name] \
|
||||
} \
|
||||
}
|
||||
|
||||
#define LOAD_FORWARDER(name, return_type, default_value, params, args) \
|
||||
s_##name = GetProcAddress(g_originalVersionDll, #name);
|
||||
|
||||
#else
|
||||
|
||||
#define DECLARE_FORWARDER(name, return_type, default_value, params, args) \
|
||||
typedef return_type (WINAPI *name##_fn) params; \
|
||||
static name##_fn s_##name; \
|
||||
return_type WINAPI name params \
|
||||
{ \
|
||||
if (s_##name == NULL) \
|
||||
{ \
|
||||
SetLastError(ERROR_PROC_NOT_FOUND); \
|
||||
return default_value; \
|
||||
} \
|
||||
return s_##name args; \
|
||||
}
|
||||
|
||||
#define LOAD_FORWARDER(name, return_type, default_value, params, args) \
|
||||
s_##name = (name##_fn)GetProcAddress(g_originalVersionDll, #name);
|
||||
|
||||
#endif
|
||||
|
||||
FOR_EACH_VERSION_FORWARDER(DECLARE_FORWARDER)
|
||||
|
||||
BOOL WINAPI GetFileVersionInfoByHandle(void)
|
||||
{
|
||||
SetLastError(ERROR_CALL_NOT_IMPLEMENTED);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static BOOL SourceInit(void)
|
||||
{
|
||||
WCHAR source[MAX_PATH];
|
||||
UINT sourceLength = GetSystemDirectoryW(source, MAX_PATH);
|
||||
|
||||
if (sourceLength == 0 || sourceLength >= MAX_PATH)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (wcscat_s(source, MAX_PATH, L"\\version.dll") != 0)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_originalVersionDll = LoadLibraryW(source);
|
||||
if (!g_originalVersionDll)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
FOR_EACH_VERSION_FORWARDER(LOAD_FORWARDER);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
|
||||
{
|
||||
(void)lpvReserved;
|
||||
|
||||
if (fdwReason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
|
||||
if (!SourceInit())
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
disable_asar_integrity();
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
LIBRARY "VERSION"
|
||||
EXPORTS
|
||||
|
||||
GetFileVersionInfoA
|
||||
GetFileVersionInfoByHandle
|
||||
GetFileVersionInfoExA
|
||||
GetFileVersionInfoExW
|
||||
GetFileVersionInfoSizeA
|
||||
GetFileVersionInfoSizeExA
|
||||
GetFileVersionInfoSizeExW
|
||||
GetFileVersionInfoSizeW
|
||||
GetFileVersionInfoW
|
||||
VerFindFileA
|
||||
VerFindFileW
|
||||
VerInstallFileA
|
||||
VerInstallFileW
|
||||
VerLanguageNameA
|
||||
VerLanguageNameW
|
||||
VerQueryValueA
|
||||
VerQueryValueW
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
# 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?
|
||||
|
||||
# Compiled lingui catalogs — generated from .po by `lingui compile`; the app
|
||||
# loads .po directly via @lingui/vite-plugin, so these are build artifacts.
|
||||
src/locales/**/*.js
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user