refactor: rebrand project to WandEnhancer

This commit is contained in:
kitbyte
2026-03-30 21:39:06 +03:00
parent 9a5d3799bf
commit 5273930eef
65 changed files with 262 additions and 250 deletions
+194
View File
@@ -0,0 +1,194 @@
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 WandEnhancer.Models;
using WandEnhancer.Utils;
using WandEnhancer.View.MainWindow;
using Application = System.Windows.Application;
namespace WandEnhancer.Core
{
public class Enhancer
{
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;
public Enhancer(WeModConfig weModConfig, Action<string, ELogType> logger, PatchConfig config)
{
_weModConfig = weModConfig;
_logger = logger;
_config = config;
_asarPath = Path.Combine(weModConfig.RootDirectory, "resources", "app.asar");
_unpackedPath = Path.Combine(weModConfig.RootDirectory, "resources", "app.asar.unpacked");
_backupPath = Path.Combine(weModConfig.RootDirectory, "resources", "app.asar.backup");
}
private string ApplyJsPatch(string fileName, string js, EnhancerConfig.PatchEntry patch, EPatchType patchType)
{
if (patch.Applied)
{
return js;
}
var matches = patch.Target.Matches(js);
if (matches.Count == 0)
{
return js;
}
var prefix = $"[ENHANCER] [{patchType} -> {patch.Name}]";
if(matches.Count > 1 && patch.SingleMatch)
{
throw new Exception(
$"{prefix} Patch failed. Multiple target functions found. Looks like the version is not supported");
}
if (patch.Resolver != null)
{
string resolvedField = patch.Resolver.Handler(matches[0].Value);
if (string.IsNullOrEmpty(resolvedField))
{
throw new Exception($"{prefix} Resolver failed to find field name");
}
patch.Patch = patch.Patch.Replace(patch.Resolver.Placeholder, resolvedField);
}
_logger($"{prefix} Found target function in: " + Path.GetFileName(fileName), ELogType.Info);
string newJs = patch.Target.Replace(js, patch.Patch);
File.WriteAllText(fileName, newJs);
_logger($"{prefix} Patch applied", ELogType.Success);
patch.Applied = true;
return newJs;
}
private void PatchAsar()
{
var items = Directory.EnumerateFiles(_unpackedPath)
.Where(file => !Directory.Exists(file) && Regex.IsMatch(Path.GetFileName(file), @"^app-\w+|index\.js"))
.ToList();
if (!items.Any())
{
throw new Exception("[ENHANCER] No app bundle found");
}
// Track patches that still need to be completed
var remainingPatches = new HashSet<EPatchType>(_config.PatchTypes);
var enhancerConfig = EnhancerConfig.GetInstance();
foreach (var item in items)
{
if (remainingPatches.Count == 0)
{
break;
}
string data = File.ReadAllText(item);
// Iterate over a copy of the list so we can modify the HashSet
foreach (var entry in remainingPatches.ToList())
{
var entries = enhancerConfig[entry];
foreach (var patchEntry in entries)
{
// Update data in memory so subsequent patches in the same file work on latest content
data = ApplyJsPatch(item, data, patchEntry, entry);
}
// Check if all entries for this patch type are applied
if (entries.All(x => x.Applied))
{
remainingPatches.Remove(entry);
}
}
}
if(remainingPatches.Count > 0)
{
var failedPatches = string.Join(", ", remainingPatches.Select(p => p.ToString()));
throw new Exception($"[ENHANCER] Failed to apply patches: {failedPatches}. The version may not be supported.");
}
}
private 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 already exists", ELogType.Warn);
}
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();
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);
}
}
}
+105
View File
@@ -0,0 +1,105 @@
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 string Name { get; set; }
public bool Applied { get; set; }
public bool SingleMatch { get; set; } = true;
public ResolveContext Resolver { get; set; }
}
public static Dictionary<EPatchType, PatchEntry[]> GetInstance()
{
return new Dictionary<EPatchType, PatchEntry[]>()
{
{
EPatchType.ActivatePro,
new[]
{
new PatchEntry
{
Resolver = new ResolveContext
{
Handler = (targetFunction) =>
{
var fetchMatch = Regex.Match(targetFunction, @"return\s+this\.#(\w+)\.fetch");
return fetchMatch.Success ? fetchMatch.Groups[1].Value : null;
},
Placeholder = "<service_name>"
},
Name = "getUserAccount",
Target = new Regex(@"getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?\}\)\}",
RegexOptions.Singleline),
Patch =
"getUserAccount(){return this.#<service_name>.fetch({endpoint:\"/v3/account\",method:\"GET\",name:\"/v3/account\",collectMetrics:0}).then(response=>{response.subscription={period:\"yearly\",state:\"active\"};return response;})}"
},
new PatchEntry
{
Resolver = new ResolveContext
{
Handler = (targetFunction) =>
{
var match = Regex.Match(targetFunction, @"return\s+this\.#(\w+)\.post");
return match.Success ? match.Groups[1].Value : null;
},
Placeholder = "<service_name>"
},
Name = "setAccountWandBrandExperience",
Target = new Regex(
@"setAccountWandBrandExperience\(\){.*?return\s+this\.#\w+\.post\(""/v3/account/brand_experience_wand""\)}",
RegexOptions.Singleline),
Patch =
"setAccountWandBrandExperience(){return this.#<service_name>.post(\"/v3/account/brand_experience_wand\").then(response=>{response.subscription={period:\"yearly\",state:\"active\"};return response;})}"
}
}
},
{
EPatchType.DisableUpdates,
new[]
{
new PatchEntry
{
Target = new Regex(@"registerHandler\(""ACTION_CHECK_FOR_UPDATE"".*?\)\)\)\)",
RegexOptions.Singleline),
Patch = "registerHandler(\"ACTION_CHECK_FOR_UPDATE\",(e=>expectUpdateFeedUrl(e,(e=>null)))"
}
}
},
{
EPatchType.DevToolsOnF12,
new[]
{
new PatchEntry
{
Resolver = new ResolveContext
{
Handler = (matchContent) => {
var match = Regex.Match(matchContent, @"this\.#(\w+)\(""ACTION_OPEN_DEV_TOOLS""\)");
return match.Success ? match.Groups[1].Value : null;
},
Placeholder = "<dispatch_method>"
},
Target = new Regex(@"document\.addEventListener\(""keydown"",\s*\((?<arg>\w+)\s*=>\s*\{[^}]*?""ACTION_OPEN_DEV_TOOLS""[^}]*?\}\)\)", RegexOptions.Singleline),
Patch = "document.addEventListener(\"keydown\",(${arg}=>{\"F12\"!==${arg}.key||this.#<dispatch_method>(\"ACTION_OPEN_DEV_TOOLS\")}))"
}
}
}
};
}
}
}
@@ -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
}
}
}
}