feat: add support for DevTools hotkey, fix patching logic

This commit is contained in:
kitbyte
2025-12-14 18:52:53 +02:00
parent a5583fd4f7
commit a943b91f5f
8 changed files with 170 additions and 59 deletions
+4 -1
View File
@@ -134,4 +134,7 @@ dist
./WeModPatcher/bin/ ./WeModPatcher/bin/
./AsarSharp/obj/ ./AsarSharp/obj/
./AsarSharp/bin/ ./AsarSharp/bin/
.idea .idea
packages
*/bin/
*/obj/
+44 -54
View File
@@ -16,35 +16,8 @@ namespace WeModPatcher.Core
{ {
public class Patcher public class Patcher
{ {
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 WeModConfig _weModConfig; private readonly WeModConfig _weModConfig;
private readonly Action<string, ELogType> _logger; private readonly Action<string, ELogType> _logger;
@@ -52,7 +25,6 @@ namespace WeModPatcher.Core
private readonly string _asarPath; private readonly string _asarPath;
private readonly string _backupPath; private readonly string _backupPath;
private readonly string _unpackedPath; private readonly string _unpackedPath;
private int _sumOfPatches = 0;
public Patcher(WeModConfig weModConfig, Action<string, ELogType> logger, PatchConfig config) public Patcher(WeModConfig weModConfig, Action<string, ELogType> logger, PatchConfig config)
{ {
@@ -65,49 +37,46 @@ namespace WeModPatcher.Core
_backupPath = Path.Combine(weModConfig.RootDirectory, "resources", "app.asar.backup"); _backupPath = Path.Combine(weModConfig.RootDirectory, "resources", "app.asar.backup");
} }
private static string GetFetchFieldName(string targetFunction) private string ApplyJsPatch(string fileName, string js, PatcherConfig.PatchEntry patch, EPatchType patchType)
{
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) if (patch.Applied)
{ {
return; return js;
} }
var matches = patch.Target.Matches(js); var matches = patch.Target.Matches(js);
if (matches.Count == 0) if (matches.Count == 0)
{ {
return; return js;
} }
var prefix = $"[PATCHER] [{patchType} -> {patch.Name}]";
if(matches.Count > 1 && patch.SingleMatch) if(matches.Count > 1 && patch.SingleMatch)
{ {
throw new Exception( throw new Exception(
$"[PATCHER] [{patchType}] Patch failed. Multiple target functions found. Looks like the version is not supported"); $"{prefix} Patch failed. Multiple target functions found. Looks like the version is not supported");
} }
if (patch.DynamicFieldResolve) if (patch.Resolver != null)
{ {
string fetchFieldName = GetFetchFieldName(matches[0].Value); string resolvedField = patch.Resolver.Handler(matches[0].Value);
if (string.IsNullOrEmpty(fetchFieldName)) if (string.IsNullOrEmpty(resolvedField))
{ {
throw new Exception($"[PATCHER] [{patchType}] Fetch field name not found"); throw new Exception($"{prefix} Resolver failed to find field name");
} }
patch.Patch = patch.Patch.Replace("<fetch_field_name>", fetchFieldName); patch.Patch = patch.Patch.Replace(patch.Resolver.Placeholder, resolvedField);
} }
_logger($"[PATCHER] [{patchType}] Found target function in: " + Path.GetFileName(fileName), ELogType.Info); _logger($"{prefix} Found target function in: " + Path.GetFileName(fileName), ELogType.Info);
File.WriteAllText(fileName, patch.Target.Replace(js, patch.Patch)); string newJs = patch.Target.Replace(js, patch.Patch);
_logger($"[PATCHER] [{patchType}] Patch applied", ELogType.Success); File.WriteAllText(fileName, newJs);
_logger($"{prefix} Patch applied", ELogType.Success);
patch.Applied = true; patch.Applied = true;
_sumOfPatches -= (int)patchType;
return newJs;
} }
private void PatchAsar() private void PatchAsar()
@@ -121,21 +90,42 @@ namespace WeModPatcher.Core
throw new Exception("[PATCHER] No app bundle found"); throw new Exception("[PATCHER] No app bundle found");
} }
var requestedPatches = _config.PatchTypes.ToList(); // Track patches that still need to be completed
requestedPatches.ForEach(patch => _sumOfPatches += (int)patch); var remainingPatches = new HashSet<EPatchType>(_config.PatchTypes);
var patcherConfig = PatcherConfig.GetInstance();
foreach (var item in items) foreach (var item in items)
{ {
if (_sumOfPatches <= 0) if (remainingPatches.Count == 0)
{ {
break; break;
} }
string data = File.ReadAllText(item); string data = File.ReadAllText(item);
foreach (var entry in requestedPatches)
// Iterate over a copy of the list so we can modify the HashSet
foreach (var entry in remainingPatches.ToList())
{ {
ApplyJsPatch(item, data, Patches[entry], entry); var entries = patcherConfig[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($"[PATCHER] Failed to apply patches: {failedPatches}. The version may not be supported.");
}
} }
private void AttachProxyDll() private void AttachProxyDll()
+105
View File
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using WeModPatcher.Models;
namespace WeModPatcher.Core
{
public static class PatcherConfig
{
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\")}))"
}
}
}
};
}
}
}
+2 -1
View File
@@ -11,7 +11,8 @@ namespace WeModPatcher.Models
{ {
ActivatePro = 1, ActivatePro = 1,
DisableUpdates = 2, DisableUpdates = 2,
DisableTelemetry = 4 DisableTelemetry = 4,
DevToolsOnF12 = 8
} }
public sealed class PatchConfig public sealed class PatchConfig
@@ -54,6 +54,12 @@
</StackPanel> </StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal"> <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button
Margin="0 0 5 0"
Width="25" Height="25" Padding="5.5"
Style="{StaticResource IconButton}"
Tag="{StaticResource CogIcon}"
/>
<Button <Button
Margin="9 0 15 0" Margin="9 0 15 0"
Tag="{StaticResource CloseIcon}" Tag="{StaticResource CloseIcon}"
@@ -24,8 +24,8 @@
<CheckBox Grid.Row="0" x:Name="ActivateProBox" HorizontalAlignment="Right" VerticalAlignment="Center" <CheckBox Grid.Row="0" x:Name="ActivateProBox" HorizontalAlignment="Right" VerticalAlignment="Center"
IsChecked="True" /> IsChecked="True" />
<TextBlock Grid.Row="1" VerticalAlignment="Center" Text="Disable telemetry" /> <TextBlock Grid.Row="1" VerticalAlignment="Center" Text="DevTools on F12" />
<CheckBox Grid.Row="1" x:Name="DisableTelemetryBox" HorizontalAlignment="Right" VerticalAlignment="Center" /> <CheckBox Grid.Row="1" x:Name="DevToolsHotkeyBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
<TextBlock Grid.Row="2" VerticalAlignment="Center" Text="Disable updates" /> <TextBlock Grid.Row="2" VerticalAlignment="Center" Text="Disable updates" />
<CheckBox Grid.Row="2" x:Name="DisableUpdateBox" HorizontalAlignment="Right" VerticalAlignment="Center" /> <CheckBox Grid.Row="2" x:Name="DisableUpdateBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
@@ -20,7 +20,7 @@ namespace WeModPatcher.View.Popups
private void OnPatchButtonClick(object sender, RoutedEventArgs e) private void OnPatchButtonClick(object sender, RoutedEventArgs e)
{ {
if (ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true && if (ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true &&
DisableTelemetryBox.IsChecked != true) DevToolsHotkeyBox.IsChecked != true)
{ {
return; return;
} }
@@ -36,6 +36,11 @@ namespace WeModPatcher.View.Popups
result.Add(EPatchType.DisableUpdates); result.Add(EPatchType.DisableUpdates);
} }
if (DevToolsHotkeyBox.IsChecked == true)
{
result.Add(EPatchType.DevToolsOnF12);
}
_onApply(new PatchConfig _onApply(new PatchConfig
{ {
PatchTypes = result, PatchTypes = result,
+1
View File
@@ -69,6 +69,7 @@
<Compile Include="Converters\BaseBooleanConverter.cs" /> <Compile Include="Converters\BaseBooleanConverter.cs" />
<Compile Include="Converters\ToVisibilityConverter.cs" /> <Compile Include="Converters\ToVisibilityConverter.cs" />
<Compile Include="Core\Patcher.cs" /> <Compile Include="Core\Patcher.cs" />
<Compile Include="Core\PatcherConfig.cs" />
<Compile Include="Models\WeModConfig.cs" /> <Compile Include="Models\WeModConfig.cs" />
<Compile Include="Models\PatchConfig.cs" /> <Compile Include="Models\PatchConfig.cs" />
<Compile Include="Models\Signature.cs" /> <Compile Include="Models\Signature.cs" />