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
+3
View File
@@ -135,3 +135,6 @@ dist
./AsarSharp/obj/
./AsarSharp/bin/
.idea
packages
*/bin/
*/obj/
+44 -54
View File
@@ -16,35 +16,8 @@ namespace WeModPatcher.Core
{
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 Action<string, ELogType> _logger;
@@ -52,7 +25,6 @@ namespace WeModPatcher.Core
private readonly string _asarPath;
private readonly string _backupPath;
private readonly string _unpackedPath;
private int _sumOfPatches = 0;
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");
}
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)
private string ApplyJsPatch(string fileName, string js, PatcherConfig.PatchEntry patch, EPatchType patchType)
{
if (patch.Applied)
{
return;
return js;
}
var matches = patch.Target.Matches(js);
if (matches.Count == 0)
{
return;
return js;
}
var prefix = $"[PATCHER] [{patchType} -> {patch.Name}]";
if(matches.Count > 1 && patch.SingleMatch)
{
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);
if (string.IsNullOrEmpty(fetchFieldName))
string resolvedField = patch.Resolver.Handler(matches[0].Value);
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));
_logger($"[PATCHER] [{patchType}] Patch applied", ELogType.Success);
string newJs = patch.Target.Replace(js, patch.Patch);
File.WriteAllText(fileName, newJs);
_logger($"{prefix} Patch applied", ELogType.Success);
patch.Applied = true;
_sumOfPatches -= (int)patchType;
return newJs;
}
private void PatchAsar()
@@ -121,20 +90,41 @@ namespace WeModPatcher.Core
throw new Exception("[PATCHER] No app bundle found");
}
var requestedPatches = _config.PatchTypes.ToList();
requestedPatches.ForEach(patch => _sumOfPatches += (int)patch);
// Track patches that still need to be completed
var remainingPatches = new HashSet<EPatchType>(_config.PatchTypes);
var patcherConfig = PatcherConfig.GetInstance();
foreach (var item in items)
{
if (_sumOfPatches <= 0)
if (remainingPatches.Count == 0)
{
break;
}
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.");
}
}
+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,
DisableUpdates = 2,
DisableTelemetry = 4
DisableTelemetry = 4,
DevToolsOnF12 = 8
}
public sealed class PatchConfig
@@ -54,6 +54,12 @@
</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}"
/>
<Button
Margin="9 0 15 0"
Tag="{StaticResource CloseIcon}"
@@ -24,8 +24,8 @@
<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="1" VerticalAlignment="Center" Text="DevTools on F12" />
<CheckBox Grid.Row="1" x:Name="DevToolsHotkeyBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
<TextBlock Grid.Row="2" VerticalAlignment="Center" Text="Disable updates" />
<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)
{
if (ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true &&
DisableTelemetryBox.IsChecked != true)
DevToolsHotkeyBox.IsChecked != true)
{
return;
}
@@ -36,6 +36,11 @@ namespace WeModPatcher.View.Popups
result.Add(EPatchType.DisableUpdates);
}
if (DevToolsHotkeyBox.IsChecked == true)
{
result.Add(EPatchType.DevToolsOnF12);
}
_onApply(new PatchConfig
{
PatchTypes = result,
+1
View File
@@ -69,6 +69,7 @@
<Compile Include="Converters\BaseBooleanConverter.cs" />
<Compile Include="Converters\ToVisibilityConverter.cs" />
<Compile Include="Core\Patcher.cs" />
<Compile Include="Core\PatcherConfig.cs" />
<Compile Include="Models\WeModConfig.cs" />
<Compile Include="Models\PatchConfig.cs" />
<Compile Include="Models\Signature.cs" />