mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-29 06:01:14 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a5d3799bf | |||
| d02b84a414 | |||
| a943b91f5f | |||
| a5583fd4f7 | |||
| 13e26c3a70 | |||
| d8d0abd448 | |||
| 54fe538ed9 | |||
| 8f14ee2939 | |||
| 9ba60a09a7 | |||
| 108feeafa8 | |||
| b283c63fd7 | |||
| 9ed3d270e3 | |||
| e534c08f88 | |||
| fcf47673af | |||
| d3ff13a528 | |||
| 123307aed7 | |||
| 68cbbee275 | |||
| 4049ade4b1 |
@@ -1 +1,2 @@
|
||||
ko_fi: kitbyte
|
||||
custom: ["https://www.paypal.com/ncp/payment/ZP3NPDYP6A34W", "https://www.paypal.com/donate/?hosted_button_id=QGGKZTFPDKMHC"]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: k1tbyte
|
||||
|
||||
---
|
||||
|
||||
**WeMod version**: X.X.X
|
||||
**Patcher version**: X.X.X
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
+14
@@ -128,3 +128,17 @@ dist
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
|
||||
./WeModPatcher/obj/
|
||||
./WeModPatcher/bin/
|
||||
./AsarSharp/obj/
|
||||
./AsarSharp/bin/
|
||||
.idea
|
||||
packages
|
||||
*/bin/
|
||||
*/obj/
|
||||
*/obj/.nuget/
|
||||
|
||||
# App settings (user preferences)
|
||||
appsettings.json
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AsarSharp.AsarFileSystem;
|
||||
using AsarSharp.Utils;
|
||||
|
||||
namespace AsarSharp
|
||||
{
|
||||
|
||||
public class CreateOptions
|
||||
{
|
||||
public Regex Unpack { get; set; }
|
||||
}
|
||||
|
||||
public class AsarCreator
|
||||
{
|
||||
private readonly string _folderPath;
|
||||
private readonly string _destPath;
|
||||
private readonly CreateOptions _options;
|
||||
private List<string> _filenames = new List<string>();
|
||||
private Dictionary<string, CrawledFileType> _metadata = new Dictionary<string, CrawledFileType>();
|
||||
|
||||
public AsarCreator(string folderPath, string destPath, CreateOptions options)
|
||||
{
|
||||
_folderPath = folderPath ?? throw new ArgumentNullException(nameof(folderPath));
|
||||
_destPath = destPath ?? throw new ArgumentNullException(nameof(destPath));
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public void CreatePackageWithOptions()
|
||||
{
|
||||
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)
|
||||
{
|
||||
HandleFile(filesystem, filename, files);
|
||||
}
|
||||
|
||||
InsertsDone(filesystem, files);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void HandleFile(Filesystem filesystem, string filename, List<Disk.BasicFileInfo> files)
|
||||
{
|
||||
if (!_metadata.ContainsKey(filename))
|
||||
{
|
||||
var fileType = FileSystemCrawler.DetermineFileType(filename);
|
||||
_metadata[filename] = fileType ?? throw new Exception("Unknown file type for file: " + filename);
|
||||
}
|
||||
var file = _metadata[filename];
|
||||
|
||||
switch (file.Type)
|
||||
{
|
||||
case FileType.Directory:
|
||||
filesystem.InsertDirectory(filename, false);
|
||||
break;
|
||||
case FileType.File:
|
||||
var shouldUnpack = ShouldUnpackPath(Extensions.GetRelativePath(_folderPath, Path.GetDirectoryName(filename)));
|
||||
files.Add(new Disk.BasicFileInfo { Filename = filename, Unpack = shouldUnpack });
|
||||
filesystem.InsertFile(filename, shouldUnpack, file);
|
||||
break;
|
||||
case FileType.Link:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUnpackPath(string relativePath)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using AsarSharp.AsarFileSystem;
|
||||
using AsarSharp.Utils;
|
||||
|
||||
namespace AsarSharp
|
||||
{
|
||||
public class AsarExtractor
|
||||
{
|
||||
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
|
||||
bool followLinks = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
// create destination directory
|
||||
Directory.CreateDirectory(dest);
|
||||
|
||||
var extractionErrors = new List<Exception>();
|
||||
foreach (var fullPath in filenames)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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(".."))
|
||||
{
|
||||
throw new InvalidOperationException($"{fullPath}: file \"{destFilename}\" writes out of the package");
|
||||
}
|
||||
|
||||
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(".."))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{fullPath}: file \"{file.Link}\" links out of the package to \"{linkSrcPath}\"");
|
||||
}
|
||||
|
||||
// On Windows, creating symlinks requires additional permissions or enabling Developer Mode,
|
||||
// so just copy the contents of the file
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// On Unix systems we use symlinks
|
||||
Directory.CreateDirectory(Extensions.GetDirectoryName(destFilename));
|
||||
Extensions.CreateSymbolicLink(linkTo, destFilename);
|
||||
}
|
||||
}
|
||||
else if (file.IsFile)
|
||||
{
|
||||
// it's a file, try to extract it
|
||||
try
|
||||
{
|
||||
byte[] content;
|
||||
|
||||
content = Disk.ReadFileSync(filesystem, filename, file);
|
||||
|
||||
File.WriteAllBytes(destFilename, content);
|
||||
|
||||
if (file.Executable == true && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Extensions.SetUnixFilePermission(destFilename, "755");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
extractionErrors.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
extractionErrors.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (extractionErrors.Count > 0)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Unable to extract some files:\n\n" +
|
||||
string.Join("\n\n", extractionErrors.Select(e => e.ToString())),
|
||||
extractionErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AsarSharp.PickleTools;
|
||||
using AsarSharp.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AsarSharp.AsarFileSystem
|
||||
{
|
||||
public static class Disk
|
||||
{
|
||||
private static Dictionary<string, Filesystem> _filesystemCache = new Dictionary<string, Filesystem>();
|
||||
|
||||
public class ArchiveHeader
|
||||
{
|
||||
public FilesystemEntry Header { get; set; }
|
||||
public string HeaderString { get; set; }
|
||||
public int HeaderSize { get; set; }
|
||||
}
|
||||
|
||||
public class FilesystemFilesAndLinks
|
||||
{
|
||||
public List<BasicFileInfo> Files { get; set; } = new List<BasicFileInfo>();
|
||||
public List<BasicFileInfo> Links { get; set; } = new List<BasicFileInfo>();
|
||||
}
|
||||
|
||||
public class BasicFileInfo
|
||||
{
|
||||
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)
|
||||
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)
|
||||
{
|
||||
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,
|
||||
HeaderString = header,
|
||||
HeaderSize = (int)size
|
||||
};
|
||||
}
|
||||
}
|
||||
public static Filesystem ReadFilesystemSync(string archivePath)
|
||||
{
|
||||
if (!_filesystemCache.ContainsKey(archivePath) || _filesystemCache[archivePath] == null)
|
||||
{
|
||||
ArchiveHeader header = ReadArchiveHeaderSync(archivePath);
|
||||
Filesystem filesystem = new Filesystem(archivePath);
|
||||
filesystem.SetHeader(header.Header, header.HeaderSize);
|
||||
_filesystemCache[archivePath] = filesystem;
|
||||
}
|
||||
|
||||
return _filesystemCache[archivePath];
|
||||
}
|
||||
|
||||
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 (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()))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
public static void UncacheAll()
|
||||
{
|
||||
_filesystemCache.Clear();
|
||||
}
|
||||
|
||||
public static void CopyFile(string dest, string rootPath, string filename)
|
||||
{
|
||||
if(dest == null || rootPath == null || filename == null)
|
||||
throw new ArgumentNullException();
|
||||
|
||||
if (dest == rootPath)
|
||||
{
|
||||
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))
|
||||
{
|
||||
sourceStream.CopyTo(destinationStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void WriteFileSystem(string dest, Filesystem fileSystem,
|
||||
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))
|
||||
{
|
||||
fs.Write(sizeBuf, 0, sizeBuf.Length);
|
||||
fs.Write(headerBuf, 0, headerBuf.Length);
|
||||
|
||||
foreach (var file in lists.Files)
|
||||
{
|
||||
if (file.Unpack)
|
||||
{
|
||||
var filename = Extensions.GetRelativePath(fileSystem.GetRootPath(), file.Filename);
|
||||
CopyFile($"{dest}.unpacked", fileSystem.GetRootPath(), filename);
|
||||
continue;
|
||||
}
|
||||
using (var transformedFileStream = new FileStream(file.Filename, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
transformedFileStream.CopyTo(fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using AsarSharp.Integrity;
|
||||
using AsarSharp.Utils;
|
||||
|
||||
namespace AsarSharp.AsarFileSystem
|
||||
{
|
||||
public class Filesystem
|
||||
{
|
||||
private readonly string _src;
|
||||
private FilesystemEntry _header;
|
||||
private int _headerSize;
|
||||
private long _offset;
|
||||
|
||||
private const uint UINT32_MAX = 0xFFFFFFFF; // 2^32 - 1
|
||||
|
||||
public Filesystem(string src)
|
||||
{
|
||||
_src = Path.GetFullPath(src);
|
||||
_header = new FilesystemEntry { Files = new Dictionary<string, FilesystemEntry>(StringComparer.Ordinal) };
|
||||
_headerSize = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
public string GetRootPath()
|
||||
{
|
||||
return _src;
|
||||
}
|
||||
|
||||
public FilesystemEntry GetHeader()
|
||||
{
|
||||
return _header;
|
||||
}
|
||||
|
||||
public int GetHeaderSize()
|
||||
{
|
||||
return _headerSize;
|
||||
}
|
||||
|
||||
public void SetHeader(FilesystemEntry header, int headerSize)
|
||||
{
|
||||
_header = header;
|
||||
_headerSize = headerSize;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (dir == "." || string.IsNullOrEmpty(dir)) continue;
|
||||
|
||||
if (json.IsDirectory)
|
||||
{
|
||||
if (!json.Files.ContainsKey(dir))
|
||||
{
|
||||
json.Files[dir] = new FilesystemEntry { Files = new Dictionary<string, FilesystemEntry>(StringComparer.Ordinal) };
|
||||
}
|
||||
json = json.Files[dir];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Unexpected directory state while traversing: {p}");
|
||||
}
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 ";
|
||||
|
||||
files.Add(isPack ? $"{packState} : {fullPath}" : fullPath);
|
||||
FillFilesFromMetadata(fullPath, childMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public static string ReadLink(string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return Path.GetFileName(path);
|
||||
// TODO , NOT IMPLEMENTED
|
||||
}
|
||||
|
||||
|
||||
|
||||
#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];
|
||||
}
|
||||
|
||||
public void InsertDirectory(string p, bool unpack)
|
||||
{
|
||||
FilesystemEntry node = SearchNodeFromPath(p);
|
||||
node.Files = node.Files ?? new Dictionary<string, FilesystemEntry>();
|
||||
node.Unpacked = unpack;
|
||||
}
|
||||
|
||||
public void InsertFile(string path, bool shouldUnpack, CrawledFileType file)
|
||||
{
|
||||
var dirName = Path.GetDirectoryName(path);
|
||||
var dirNode = SearchNodeFromPath(dirName);
|
||||
var node = SearchNodeFromPath(path);
|
||||
|
||||
long size = 0;
|
||||
if (file.Stat is FileInfo fileInfo)
|
||||
{
|
||||
size = fileInfo.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);
|
||||
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);
|
||||
if (!Extensions.IsWindowsPlatform() && (file.Stat.Attributes & FileAttributes.Hidden) != 0)
|
||||
{
|
||||
node.Executable = true;
|
||||
}
|
||||
_offset += size;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AsarSharp.Utils;
|
||||
|
||||
namespace AsarSharp.AsarFileSystem
|
||||
{
|
||||
public class CrawledFileType
|
||||
{
|
||||
public FileType Type { get; set; }
|
||||
public FileSystemInfo Stat { get; set; }
|
||||
public TransformedFile Transformed { get; set; }
|
||||
}
|
||||
|
||||
public class TransformedFile
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public FileSystemInfo Stat { get; set; }
|
||||
}
|
||||
|
||||
public enum FileType
|
||||
{
|
||||
File,
|
||||
Directory,
|
||||
Link
|
||||
}
|
||||
|
||||
public static class FileSystemCrawler
|
||||
{
|
||||
|
||||
|
||||
public static CrawledFileType DetermineFileType(string filename)
|
||||
{
|
||||
var fileInfo = new FileInfo(filename);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
return new CrawledFileType { Type = FileType.File, Stat = fileInfo };
|
||||
}
|
||||
|
||||
var directoryInfo = new DirectoryInfo(filename);
|
||||
if (directoryInfo.Exists)
|
||||
{
|
||||
return new CrawledFileType { Type = FileType.Directory, Stat = directoryInfo };
|
||||
}
|
||||
|
||||
var linkInfo = new FileInfo(filename);
|
||||
if (linkInfo.Exists && (linkInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||
{
|
||||
return new CrawledFileType { Type = FileType.Link, Stat = linkInfo };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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>();
|
||||
|
||||
foreach (var result in results.Where(result => result.type != null))
|
||||
{
|
||||
metadata[result.filename] = result.type;
|
||||
if (result.type.Type == FileType.Link)
|
||||
{
|
||||
links.Add(result.filename);
|
||||
}
|
||||
filenames.Add(result.filename);
|
||||
}
|
||||
|
||||
var filteredFilenames = new List<string>();
|
||||
|
||||
foreach (var filename in filenames)
|
||||
{
|
||||
var exactLinkIndex = links.FindIndex(link => filename == link);
|
||||
var isValid = true;
|
||||
|
||||
for (var i = 0; i < links.Count; i++)
|
||||
{
|
||||
if (i == exactLinkIndex)
|
||||
{
|
||||
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))
|
||||
{
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid)
|
||||
{
|
||||
filteredFilenames.Add(filename);
|
||||
}
|
||||
}
|
||||
|
||||
return (filteredFilenames, metadata);
|
||||
}
|
||||
|
||||
|
||||
// (File order is not important!!!)
|
||||
public static List<string> CrawlIterative(string dir)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var stack = new Stack<string>();
|
||||
|
||||
|
||||
string basePath = Extensions.GetBasePath(dir);
|
||||
|
||||
if (!Directory.Exists(basePath))
|
||||
return result;
|
||||
|
||||
// Add only the base directory to the stack, but not to the result
|
||||
stack.Push(basePath);
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
string currentDir = stack.Pop();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// Skip directories to which there is no access
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using AsarSharp.Integrity;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AsarSharp.AsarFileSystem
|
||||
{
|
||||
public class FilesystemEntry
|
||||
{
|
||||
[JsonProperty("files")]
|
||||
public Dictionary<string, FilesystemEntry> Files { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("executable")]
|
||||
public bool? Executable { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long? Size { get; set; }
|
||||
|
||||
[JsonProperty("offset")]
|
||||
public string Offset { get; set; }
|
||||
|
||||
[JsonProperty("unpacked")]
|
||||
public bool? Unpacked { get; set; }
|
||||
|
||||
[JsonProperty("integrity")]
|
||||
public IntegrityHelper.FileIntegrity Integrity { get; set; }
|
||||
|
||||
[JsonProperty("link")]
|
||||
public string Link { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsDirectory => Files != null;
|
||||
[JsonIgnore]
|
||||
public bool IsFile => Size.HasValue;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsLink => Link != null;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Offset: {Offset}, Size: {Size} Unpacked: {Unpacked}";
|
||||
}
|
||||
|
||||
public bool ShouldSerializeUnpacked()
|
||||
{
|
||||
return Unpacked == true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"
|
||||
Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{BEAA604A-402A-4387-8903-A53FC913A26E}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>AsarSharp</RootNamespace>
|
||||
<AssemblyName>AsarSharp</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System"/>
|
||||
<Reference Include="System.Core"/>
|
||||
<Reference Include="System.Data"/>
|
||||
<Reference Include="System.Xml"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AsarCreator.cs" />
|
||||
<Compile Include="AsarExtractor.cs" />
|
||||
<Compile Include="AsarFileSystem\Disk.cs" />
|
||||
<Compile Include="AsarFileSystem\FileSystem.cs" />
|
||||
<Compile Include="AsarFileSystem\FileSystemCrawler.cs" />
|
||||
<Compile Include="AsarFileSystem\FilesystemEntry.cs" />
|
||||
<Compile Include="Integrity\IntegrityHelper.cs" />
|
||||
<Compile Include="PickleTools\Pickle.cs" />
|
||||
<Compile Include="PickleTools\PickleIterator.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs"/>
|
||||
<Compile Include="Utils\Extensions.cs" />
|
||||
<Compile Include="Utils\NativeMethods.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"/>
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
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 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)
|
||||
{
|
||||
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using(var fileHash = SHA256.Create())
|
||||
{
|
||||
|
||||
var blockHashes = new List<string>();
|
||||
var buffer = new byte[BLOCK_SIZE];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = fileStream.Read(buffer, 0, BLOCK_SIZE)) > 0)
|
||||
{
|
||||
var block = new byte[bytesRead];
|
||||
Array.Copy(buffer, block, bytesRead);
|
||||
blockHashes.Add(HashBlock(block));
|
||||
fileHash.TransformBlock(block, 0, block.Length, null, 0);
|
||||
}
|
||||
|
||||
fileHash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
|
||||
return new FileIntegrity
|
||||
{
|
||||
Algorithm = ALGORITHM,
|
||||
Hash = BitConverter.ToString(fileHash.Hash).Replace("-", "").ToLowerInvariant(),
|
||||
BlockSize = BLOCK_SIZE,
|
||||
Blocks = blockHashes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string HashBlock(byte[] block)
|
||||
{
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
var hash = sha256.ComputeHash(block);
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace AsarSharp.PickleTools
|
||||
{
|
||||
public class Pickle
|
||||
{
|
||||
public const int SIZE_INT32 = 4;
|
||||
public const int SIZE_UINT32 = 4;
|
||||
public const int SIZE_INT64 = 8;
|
||||
public const int SIZE_UINT64 = 8;
|
||||
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;
|
||||
|
||||
// Maximum value for read-only
|
||||
public const long CAPACITY_READ_ONLY = 9007199254740992;
|
||||
|
||||
private byte[] _header;
|
||||
private int _headerSize;
|
||||
private long _capacityAfterHeader;
|
||||
private int _writeOffset;
|
||||
|
||||
private Pickle(byte[] buffer = null)
|
||||
{
|
||||
if (buffer != null)
|
||||
{
|
||||
_header = buffer;
|
||||
_headerSize = buffer.Length - GetPayloadSize();
|
||||
_capacityAfterHeader = CAPACITY_READ_ONLY;
|
||||
_writeOffset = 0;
|
||||
|
||||
if (_headerSize > buffer.Length)
|
||||
{
|
||||
_headerSize = 0;
|
||||
}
|
||||
|
||||
if (_headerSize != AlignInt(_headerSize, SIZE_UINT32))
|
||||
{
|
||||
_headerSize = 0;
|
||||
}
|
||||
|
||||
if (_headerSize == 0)
|
||||
{
|
||||
_header = new byte[0];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_header = new byte[0];
|
||||
_headerSize = SIZE_UINT32;
|
||||
_capacityAfterHeader = 0;
|
||||
_writeOffset = 0;
|
||||
Resize(PAYLOAD_UNIT);
|
||||
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 int GetHeaderSize()
|
||||
{
|
||||
return _headerSize;
|
||||
}
|
||||
|
||||
public PickleIterator CreateIterator()
|
||||
{
|
||||
return new PickleIterator(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts Pickle to a byte array
|
||||
/// </summary>
|
||||
public byte[] ToBuffer()
|
||||
{
|
||||
int resultSize = _headerSize + GetPayloadSize();
|
||||
byte[] result = new byte[resultSize];
|
||||
Array.Copy(_header, 0, result, 0, resultSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteBool(bool value)
|
||||
{
|
||||
return WriteInt(value ? 1 : 0);
|
||||
}
|
||||
|
||||
public bool WriteInt(int value)
|
||||
{
|
||||
EnsureCapacity(SIZE_INT32);
|
||||
|
||||
var dataLength = AlignInt(SIZE_INT32, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteUInt32(uint value)
|
||||
{
|
||||
EnsureCapacity(SIZE_UINT32);
|
||||
|
||||
var dataLength = AlignInt(SIZE_UINT32, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
public bool WriteUInt64(ulong value)
|
||||
{
|
||||
EnsureCapacity(SIZE_UINT64);
|
||||
|
||||
var dataLength = AlignInt(SIZE_UINT64, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool WriteString(string value)
|
||||
{
|
||||
byte[] strBytes = Encoding.UTF8.GetBytes(value);
|
||||
int length = strBytes.Length;
|
||||
|
||||
if (!WriteInt(length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dataLength = AlignInt(length, SIZE_UINT32);
|
||||
var newSize = _writeOffset + dataLength;
|
||||
|
||||
if (newSize > _capacityAfterHeader)
|
||||
{
|
||||
Resize(Math.Max((int)_capacityAfterHeader * 2, newSize));
|
||||
}
|
||||
|
||||
Array.Copy(strBytes, 0, _header, _headerSize + _writeOffset, length);
|
||||
|
||||
var endOffset = _headerSize + _writeOffset + length;
|
||||
for (int i = endOffset; i < endOffset + dataLength - length; i++)
|
||||
{
|
||||
_header[i] = 0;
|
||||
}
|
||||
|
||||
SetPayloadSize(newSize);
|
||||
_writeOffset = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPayloadSize(int payloadSize)
|
||||
{
|
||||
WriteUInt32LE((uint)payloadSize, 0);
|
||||
}
|
||||
|
||||
public int GetPayloadSize()
|
||||
{
|
||||
return (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);
|
||||
_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));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace AsarSharp.PickleTools
|
||||
{
|
||||
public class PickleIterator
|
||||
{
|
||||
private readonly byte[] _payload;
|
||||
private readonly int _payloadOffset;
|
||||
private int _readIndex;
|
||||
private readonly int _endIndex;
|
||||
|
||||
public PickleIterator(Pickle pickle)
|
||||
{
|
||||
_payload = pickle.GetHeader();
|
||||
_payloadOffset = pickle.GetHeaderSize();
|
||||
_readIndex = 0;
|
||||
_endIndex = pickle.GetPayloadSize();
|
||||
}
|
||||
|
||||
public bool ReadBool()
|
||||
{
|
||||
return ReadInt() != 0;
|
||||
}
|
||||
|
||||
public int ReadInt()
|
||||
{
|
||||
return ReadBytes(Pickle.SIZE_INT32, BitConverter.ToInt32);
|
||||
}
|
||||
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
return ReadBytes(Pickle.SIZE_UINT32, BitConverter.ToUInt32);
|
||||
}
|
||||
|
||||
public long ReadInt64()
|
||||
{
|
||||
return ReadBytes(Pickle.SIZE_INT64, BitConverter.ToInt64);
|
||||
}
|
||||
|
||||
public ulong ReadUInt64()
|
||||
{
|
||||
return ReadBytes(Pickle.SIZE_UINT64, BitConverter.ToUInt64);
|
||||
}
|
||||
|
||||
public float ReadFloat()
|
||||
{
|
||||
return ReadBytes(Pickle.SIZE_FLOAT, BitConverter.ToSingle);
|
||||
}
|
||||
|
||||
public double ReadDouble()
|
||||
{
|
||||
return ReadBytes(Pickle.SIZE_DOUBLE, BitConverter.ToDouble);
|
||||
}
|
||||
|
||||
public string ReadString()
|
||||
{
|
||||
int length = ReadInt();
|
||||
return Encoding.UTF8.GetString(ReadBytes(length));
|
||||
}
|
||||
|
||||
private T ReadBytes<T>(int length, Func<byte[], int, T> converter)
|
||||
{
|
||||
int readPayloadOffset = GetReadPayloadOffsetAndAdvance(length);
|
||||
return converter(_payload, readPayloadOffset);
|
||||
}
|
||||
|
||||
private byte[] ReadBytes(int length)
|
||||
{
|
||||
int readPayloadOffset = GetReadPayloadOffsetAndAdvance(length);
|
||||
byte[] result = new byte[length];
|
||||
Array.Copy(_payload, readPayloadOffset, result, 0, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private int GetReadPayloadOffsetAndAdvance(int length)
|
||||
{
|
||||
if (length > _endIndex - _readIndex)
|
||||
{
|
||||
_readIndex = _endIndex;
|
||||
throw new InvalidOperationException($"Failed to read data with length of {length}");
|
||||
}
|
||||
int readPayloadOffset = _payloadOffset + _readIndex;
|
||||
Advance(length);
|
||||
return readPayloadOffset;
|
||||
}
|
||||
|
||||
private void Advance(int size)
|
||||
{
|
||||
int alignedSize = Pickle.AlignInt(size, Pickle.SIZE_UINT32);
|
||||
if (_endIndex - _readIndex < alignedSize)
|
||||
{
|
||||
_readIndex = _endIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
_readIndex += alignedSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 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("AsarSharp")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("AsarSharp")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("BEAA604A-402A-4387-8903-A53FC913A26E")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// 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.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AsarSharp.Utils
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
public static string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativeTo))
|
||||
throw new ArgumentNullException(nameof(relativeTo));
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
var fullRelativeTo = Path.GetFullPath(relativeTo);
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
|
||||
if (string.Equals(fullRelativeTo, fullPath, StringComparison.OrdinalIgnoreCase))
|
||||
return "";
|
||||
|
||||
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() : ""));
|
||||
|
||||
var relativeUri = relativeToUri.MakeRelativeUri(pathUri);
|
||||
var relativePath = Uri.UnescapeDataString(relativeUri.ToString())
|
||||
.Replace('/', Path.DirectorySeparatorChar);
|
||||
|
||||
return relativePath.TrimEnd(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
FileName = "chmod",
|
||||
Arguments = $"{permission} \"{filePath}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
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
|
||||
: NativeMethods.SymLinkFlag.File);
|
||||
return;
|
||||
}
|
||||
|
||||
// In Unix systems we use the corresponding system call
|
||||
var process = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "ln",
|
||||
Arguments = $"-s \"{linkTarget}\" \"{linkPath}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
|
||||
public static bool IsWindowsPlatform()
|
||||
{
|
||||
return Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AsarSharp.Utils
|
||||
{
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymLinkFlag dwFlags);
|
||||
|
||||
public enum SymLinkFlag
|
||||
{
|
||||
File = 0,
|
||||
Directory = 1,
|
||||
AllowUnprivilegedCreate = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# Contributing to WeMod Patcher
|
||||
|
||||
Thank you for your interest in the WeMod Patcher project! This document provides guidelines for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Development Environment Setup](#development-environment-setup)
|
||||
- [Bug Reports](#bug-reports)
|
||||
- [Feature Suggestions](#feature-suggestions)
|
||||
- [Creating a Pull Request](#creating-a-pull-request)
|
||||
- [Code Style](#code-style)
|
||||
- [Testing](#testing)
|
||||
- [License](#license)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
By participating in this project, you commit to maintaining respectful interactions with all community members. Any form of insults, harassment, or other unacceptable behavior will not be tolerated.
|
||||
|
||||
## Project Structure
|
||||
|
||||
The project consists of the following main components:
|
||||
|
||||
- **WeModPatcher** - Main project containing the patcher logic and user interface
|
||||
- **AsarSharp** - Library for working with ASAR archives (used for unpacking and modifying WeMod files)
|
||||
- **Core** - Core of the patcher, including static and dynamic patching
|
||||
- **Models** - Data models used in the project
|
||||
- **View** - User interface components
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/k1tbyte/Wemod-Patcher.git
|
||||
```
|
||||
|
||||
2. Open the solution `Wemod-Patcher.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:
|
||||
|
||||
- WeMod Patcher version
|
||||
- WeMod version where the problem occurred
|
||||
- Detailed steps to reproduce the bug
|
||||
- Expected and actual behavior
|
||||
- Screenshots or error logs (if available)
|
||||
|
||||
## Feature Suggestions
|
||||
|
||||
Suggestions for new features or improvements are welcome! Create an Issue describing your idea, explaining:
|
||||
|
||||
- What problem the proposed improvement solves
|
||||
- How you envision implementing this feature
|
||||
- Potential alternatives you've considered
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a branch with a descriptive name:
|
||||
```
|
||||
git checkout -b feature/feature-name
|
||||
```
|
||||
or
|
||||
```
|
||||
git checkout -b fix/fix-name
|
||||
```
|
||||
|
||||
3. Make the necessary changes and commit with clear, descriptive messages.
|
||||
|
||||
4. Ensure your code follows the project's style.
|
||||
|
||||
5. Push the branch to your fork:
|
||||
```
|
||||
git push origin your-branch-name
|
||||
```
|
||||
|
||||
6. Create a Pull Request to the main repository.
|
||||
|
||||
7. In the Pull Request description, explain the changes made and why they're necessary.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Use C# naming conventions:
|
||||
- PascalCase for class, method, and property names
|
||||
- camelCase for local variables and parameters
|
||||
- _camelCase for private fields
|
||||
|
||||
- Add comments for complex code sections or patching methods
|
||||
|
||||
- Follow SOLID and DRY principles
|
||||
|
||||
## Testing
|
||||
|
||||
Before submitting a Pull Request, ensure that:
|
||||
|
||||
1. Your code compiles without errors
|
||||
2. You've manually tested the functionality
|
||||
3. The patch works with the current version of WeMod
|
||||
4. Changes don't break existing functionality
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the [Apache License 2.0](LICENSE.md).
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to the WeMod Patcher project!
|
||||
@@ -1,4 +1,8 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
---
|
||||
<h1>WeMod Patcher</h1>
|
||||
</div>
|
||||
|
||||
@@ -15,11 +19,14 @@ With this patch you will be able to use the latest version together with Pro.
|
||||
## 💫 What features will be available?
|
||||
|
||||
✅ Unlimited usage time <br/>
|
||||
✅ No ads <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/>
|
||||
❌ Hotkeys (hotkey functionality breaks after patch for unknown reason)
|
||||
|
||||
|
||||
## 👀 How to use?
|
||||
|
||||
@@ -34,15 +41,28 @@ With this patch you will be able to use the latest version together with Pro.
|
||||
- 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 patch, you have to turn off hotkeys in WeMod settings
|
||||
- Why is the patch executable file size so large? It seems to me that you want to harm my system.
|
||||
- The thing is that the application is written in Electron, so it also puts chromium, nodejs and some libraries in the exe. Maybe Electron is a temporary solution and in the future I will consider another option
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Screenshots
|
||||

|
||||

|
||||
---
|
||||
|
||||
## 📜 License
|
||||
This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.txt) file for details.
|
||||
This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.md) file for details.
|
||||
|
||||
---
|
||||
## ❤️ Support
|
||||
[](https://ko-fi.com/kitbyte)
|
||||
|
||||
---
|
||||
|
||||
[](https://www.star-history.com/#k1tbyte/Wemod-Patcher&Date)
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
</configuration>
|
||||
@@ -0,0 +1,21 @@
|
||||
<Application x:Class="WeModPatcher.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">
|
||||
<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"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<FontFamily x:Key="Inter" >pack://application:,,,/Style/#Inter 18pt 18pt</FontFamily>
|
||||
|
||||
<converters:ToVisibilityConverter x:Key="ToVisibilityConverter"/>
|
||||
<converters:ToVisibilityInvertedConverter x:Key="ToVisibilityInvertedConverter"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using WeModPatcher.Core;
|
||||
using WeModPatcher.Core.Services;
|
||||
using WeModPatcher.View.MainWindow;
|
||||
using MessageBox = System.Windows.Forms.MessageBox;
|
||||
|
||||
namespace WeModPatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App
|
||||
{
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
LocalizationManager.Initialize();
|
||||
this.MainWindow.Show();
|
||||
}
|
||||
|
||||
public new static void Shutdown()
|
||||
{
|
||||
Current.Dispatcher.Invoke(() => Current.Shutdown());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using WeModPatcher.Models;
|
||||
|
||||
namespace WeModPatcher
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const string RepoName = "Wemod-Patcher";
|
||||
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(
|
||||
"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)
|
||||
// jnz short loc_1403A4DD2 (Integrity check failed)
|
||||
// call near ptr funk_1445527E0
|
||||
// ...
|
||||
private const string PatchSignature = "E8 ?? ?? ?? ?? ?? C0 75 ?? F6 C3 01 74 ?? 48 89 F9 E8 ?? ?? ?? ??";
|
||||
private static readonly byte[] PatchBytes = { 0x31 };
|
||||
private const int PatchOffset = 0x5;*/
|
||||
|
||||
static Constants()
|
||||
{
|
||||
Version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace WeModPatcher.Converters
|
||||
{
|
||||
public abstract class BaseBooleanConverter<T> : IValueConverter
|
||||
{
|
||||
protected BaseBooleanConverter(T trueValue, T falseValue)
|
||||
{
|
||||
True = trueValue;
|
||||
False = falseValue;
|
||||
}
|
||||
|
||||
protected T True { get; set; }
|
||||
protected T False { get; set; }
|
||||
|
||||
public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
return False;
|
||||
case bool booleanValue:
|
||||
return booleanValue ? True : False;
|
||||
}
|
||||
|
||||
if (!(value is int intValue))
|
||||
{
|
||||
return True;
|
||||
}
|
||||
|
||||
switch (parameter)
|
||||
{
|
||||
case null:
|
||||
return intValue == 0 ? False : True;
|
||||
case int param:
|
||||
return intValue > param ? True : False;
|
||||
default:
|
||||
//Because object not null
|
||||
return True;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is T t && EqualityComparer<T>.Default.Equals(t, True);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace WeModPatcher.Converters
|
||||
{
|
||||
internal sealed class ToVisibilityConverter : BaseBooleanConverter<Visibility>
|
||||
{
|
||||
public ToVisibilityConverter() :
|
||||
base(Visibility.Visible, Visibility.Collapsed)
|
||||
{ }
|
||||
}
|
||||
|
||||
internal sealed class ToVisibilityInvertedConverter : BaseBooleanConverter<Visibility>
|
||||
{
|
||||
public ToVisibilityInvertedConverter() :
|
||||
base(Visibility.Collapsed, Visibility.Visible)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -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 WeModPatcher.Models;
|
||||
using WeModPatcher.Utils;
|
||||
using WeModPatcher.View.MainWindow;
|
||||
using Application = System.Windows.Application;
|
||||
|
||||
namespace WeModPatcher.Core
|
||||
{
|
||||
public class Patcher
|
||||
{
|
||||
|
||||
|
||||
|
||||
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 Patcher(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, PatcherConfig.PatchEntry patch, EPatchType patchType)
|
||||
{
|
||||
if (patch.Applied)
|
||||
{
|
||||
return js;
|
||||
}
|
||||
|
||||
var matches = patch.Target.Matches(js);
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
return js;
|
||||
}
|
||||
|
||||
var prefix = $"[PATCHER] [{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("[PATCHER] No app bundle found");
|
||||
}
|
||||
|
||||
// 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 (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 = 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()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var dll = assembly.GetManifestResourceStream(Constants.ProxyDllResouceName);
|
||||
if (dll == null)
|
||||
{
|
||||
throw new Exception("[PATCHER] Proxy DLL resource not found");
|
||||
}
|
||||
var destPath = Path.Combine(_weModConfig.RootDirectory, "version.dll");
|
||||
using (var fileStream = File.Create(destPath))
|
||||
{
|
||||
dll.CopyTo(fileStream);
|
||||
}
|
||||
_logger("[PATCHER] Proxy DLL attached", ELogType.Info);
|
||||
}
|
||||
|
||||
public void Patch()
|
||||
{
|
||||
Common.TryKillProcess(_weModConfig.BrandName);
|
||||
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}");
|
||||
}
|
||||
|
||||
AttachProxyDll();
|
||||
|
||||
_logger("[PATCHER] Done!", ELogType.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\")}))"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 WeModPatcher.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 WeModPatcher.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,41 @@
|
||||
<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">WeMod Patcher</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">Patch</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Starten</s:String>
|
||||
<s:String x:Key="pv_popup_title">Was werden wir patchen?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Vor dem Update wird dringend empfohlen, Patches rückgängig zu machen, falls sie angewendet wurden</s:String>
|
||||
<s:String x:Key="up_update_now">Jetzt aktualisieren</s:String>
|
||||
<s:String x:Key="up_popup_title">Update verfügbar!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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">Patch</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Start</s:String>
|
||||
<s:String x:Key="pv_popup_title">What are we gonna patch?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Before updating, it is strongly recommended to roll back patches if they have been applied</s:String>
|
||||
<s:String x:Key="up_update_now">Update now</s:String>
|
||||
<s:String x:Key="up_popup_title">Update available!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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">Parchear</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Iniciar</s:String>
|
||||
<s:String x:Key="pv_popup_title">¿Qué vamos a parchear?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Antes de actualizar, se recomienda encarecidamente revertir los parches si se han aplicado</s:String>
|
||||
<s:String x:Key="up_update_now">Actualizar ahora</s:String>
|
||||
<s:String x:Key="up_popup_title">¡Actualización disponible!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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">Patcher</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Démarrer</s:String>
|
||||
<s:String x:Key="pv_popup_title">Qu'allons-nous patcher ?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Avant la mise à jour, il est fortement recommandé d'annuler les patchs s'ils ont été appliqués</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,41 @@
|
||||
<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">WeMod Patcher</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">Patch</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Avvia</s:String>
|
||||
<s:String x:Key="pv_popup_title">Cosa patcheremo?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Prima dell'aggiornamento, si consiglia vivamente di annullare le patch se sono state applicate</s:String>
|
||||
<s:String x:Key="up_update_now">Aggiorna ora</s:String>
|
||||
<s:String x:Key="up_popup_title">Aggiornamento disponibile!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">開始</s:String>
|
||||
<s:String x:Key="pv_popup_title">何をパッチしますか?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">アップデート前に、パッチが適用されている場合はロールバックすることを強くお勧めします</s:String>
|
||||
<s:String x:Key="up_update_now">今すぐ更新</s:String>
|
||||
<s:String x:Key="up_popup_title">アップデート利用可能!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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">Patchuj</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Rozpocznij</s:String>
|
||||
<s:String x:Key="pv_popup_title">Co będziemy patchować?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Przed aktualizacją zdecydowanie zaleca się cofnięcie patchów, jeśli zostały zastosowane</s:String>
|
||||
<s:String x:Key="up_update_now">Aktualizuj teraz</s:String>
|
||||
<s:String x:Key="up_popup_title">Dostępna aktualizacja!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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">Patch</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Iniciar</s:String>
|
||||
<s:String x:Key="pv_popup_title">O que vamos patchear?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Antes de atualizar, é altamente recomendável reverter os patches se eles foram aplicados</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,41 @@
|
||||
<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">WeMod Patcher</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Начать</s:String>
|
||||
<s:String x:Key="pv_popup_title">Что будем патчить?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Перед обновлением настоятельно рекомендуется откатить патчи, если они были применены</s:String>
|
||||
<s:String x:Key="up_update_now">Обновить сейчас</s:String>
|
||||
<s:String x:Key="up_popup_title">Доступно обновление!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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">Yama</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Başlat</s:String>
|
||||
<s:String x:Key="pv_popup_title">Ne yamalayacağız?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Güncellemeden önce, yamalar uygulandıysa geri almak şiddetle tavsiye edilir</s:String>
|
||||
<s:String x:Key="up_update_now">Şimdi güncelle</s:String>
|
||||
<s:String x:Key="up_popup_title">Güncelleme mevcut!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">Почати</s:String>
|
||||
<s:String x:Key="pv_popup_title">Що будемо патчити?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">Перед оновленням наполегливо рекомендується відкотити патчі, якщо вони були застосовані</s:String>
|
||||
<s:String x:Key="up_update_now">Оновити зараз</s:String>
|
||||
<s:String x:Key="up_popup_title">Доступне оновлення!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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">WeMod Patcher</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>
|
||||
<!--#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 PatchVectorsPopup -->
|
||||
<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_start">开始</s:String>
|
||||
<s:String x:Key="pv_popup_title">我们要打什么补丁?</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
<!--#region UpdatePopup -->
|
||||
<s:String x:Key="up_warning">在更新之前,强烈建议回滚已应用的补丁</s:String>
|
||||
<s:String x:Key="up_update_now">立即更新</s:String>
|
||||
<s:String x:Key="up_popup_title">有更新可用!</s:String>
|
||||
<!--#endregion -->
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using WeModPatcher.Utils;
|
||||
|
||||
namespace WeModPatcher.Models
|
||||
{
|
||||
|
||||
public enum EPatchType
|
||||
{
|
||||
ActivatePro = 1,
|
||||
DisableUpdates = 2,
|
||||
DisableTelemetry = 4,
|
||||
DevToolsOnF12 = 8
|
||||
}
|
||||
|
||||
public sealed class PatchConfig
|
||||
{
|
||||
private string _path;
|
||||
public HashSet<EPatchType> PatchTypes { get; set; }
|
||||
|
||||
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 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)
|
||||
{
|
||||
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 WeModPatcher.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeModPatcher.View.MainWindow;
|
||||
|
||||
namespace WeModPatcher
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
List<LogEntry> logEntries = new List<LogEntry>();
|
||||
if (args.Length > 0)
|
||||
{
|
||||
// TODO: Command line arguments handling
|
||||
}
|
||||
|
||||
var application = new App();
|
||||
application.InitializeComponent();
|
||||
application.MainWindow = new MainWindow();
|
||||
foreach (var logEntry in logEntries)
|
||||
{
|
||||
MainWindow.Instance.ViewModel.LogList.Add(logEntry);
|
||||
}
|
||||
application.Run();
|
||||
}
|
||||
|
||||
|
||||
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
MessageBox.Show(e.Exception.ToString());
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
MessageBox.Show(e.ExceptionObject.ToString());
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
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: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("WeModPatcher")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
//In order to begin building localizable applications, set
|
||||
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
|
||||
//inside a <PropertyGroup>. For example, if you are using US english
|
||||
//in your source files, set the <UICulture> to en-US. Then uncomment
|
||||
//the NeutralResourceLanguage attribute below. Update the "en-US" in
|
||||
//the line below to match the UICulture setting in the project file.
|
||||
|
||||
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
|
||||
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// 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.6.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.6.0")]
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace WeModPatcher.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder",
|
||||
"4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources
|
||||
{
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance",
|
||||
"CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
||||
.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((resourceMan == null))
|
||||
{
|
||||
global::System.Resources.ResourceManager temp =
|
||||
new global::System.Resources.ResourceManager("WeModPatcher.Properties.Resources",
|
||||
typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState
|
||||
.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture
|
||||
{
|
||||
get { return resourceCulture; }
|
||||
set { resourceCulture = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace WeModPatcher.ReactiveUICore
|
||||
{
|
||||
public sealed class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<object, Task> _execute;
|
||||
private readonly Func<object, bool> _canExecute;
|
||||
|
||||
private long _isExecuting;
|
||||
|
||||
public AsyncRelayCommand(Func<object, Task> execute, Func<object, bool> canExecute = null)
|
||||
{
|
||||
this._execute = execute;
|
||||
this._canExecute = canExecute ?? (o => true);
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add => CommandManager.RequerySuggested += value;
|
||||
remove => CommandManager.RequerySuggested -= value;
|
||||
}
|
||||
|
||||
private static void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested();
|
||||
|
||||
public bool CanExecute(object parameter) => Interlocked.Read(ref _isExecuting) == 0 && _canExecute(parameter);
|
||||
|
||||
public async void Execute(object parameter)
|
||||
{
|
||||
Interlocked.Exchange(ref _isExecuting, 1);
|
||||
RaiseCanExecuteChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _execute(parameter);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _isExecuting, 0);
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace WeModPatcher.ReactiveUICore
|
||||
{
|
||||
public class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
|
||||
protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace WeModPatcher.ReactiveUICore
|
||||
{
|
||||
public sealed class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object> _execute;
|
||||
private readonly Func<object, bool> _canExecute;
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add => CommandManager.RequerySuggested += value;
|
||||
remove => CommandManager.RequerySuggested -= value;
|
||||
}
|
||||
|
||||
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
|
||||
{
|
||||
_execute = execute;
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
|
||||
public void Execute(object parameter) => _execute(parameter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Brush x:Key="Background">#09090b</Brush>
|
||||
<Brush x:Key="Foreground">#FAFAFA</Brush>
|
||||
<Brush x:Key="Card">#27272A</Brush>
|
||||
<Brush x:Key="CardForeground">#FAFAFA</Brush>
|
||||
<Brush x:Key="Popover">#09090b</Brush>
|
||||
<Brush x:Key="PopoverForeground">#FAFAFA</Brush>
|
||||
<Brush x:Key="Primary">#FAFAFA</Brush>
|
||||
<Brush x:Key="PrimaryForeground">#18181B</Brush>
|
||||
<Brush x:Key="Secondary">#27272A</Brush>
|
||||
<Brush x:Key="SecondaryForeground">#FAFAFA</Brush>
|
||||
<Brush x:Key="Muted">#18181a</Brush>
|
||||
<Brush x:Key="MutedForeground">#A1A1AA</Brush>
|
||||
<Brush x:Key="Accent">#27272A</Brush>
|
||||
<Brush x:Key="AccentForeground">#FAFAFA</Brush>
|
||||
<Brush x:Key="Destructive">Red</Brush>
|
||||
<Brush x:Key="DestructiveForeground">#FAFAFA</Brush>
|
||||
<Brush x:Key="Border">#27272A</Brush>
|
||||
<Brush x:Key="Input">#27272A</Brush>
|
||||
<Brush x:Key="Ring">#D4D4D8</Brush>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,34 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Geometry x:Key="CloseIcon">
|
||||
M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z
|
||||
</Geometry>
|
||||
|
||||
<Geometry x:Key="CogIcon">
|
||||
M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z
|
||||
</Geometry>
|
||||
|
||||
<Geometry x:Key="Logo">
|
||||
M47.845,22.185l-20.03,-20.03c-1.543,-1.543 -4.046,-1.553 -5.729,0.002l-19.931,20.028c-1.542,1.542 -1.554,4.045 0,5.727l19.934,19.934c0.772,0.772 1.785,1.16 2.816,1.16c1.026,0 2.07,-0.385 2.91,-1.16l19.933,-19.934c1.605,-1.605 1.648,-4.175 0.097,-5.727zM18,27c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2zM25,34c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2zM25,20c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2zM32,27c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2z
|
||||
</Geometry>
|
||||
|
||||
<Geometry x:Key="GitHub">
|
||||
M12 2A10 10 0 0122 12c0 4.42-2.86 8.16-6.83 9.5-.51.09-.67-.23-.67-.5 0-.32 0-1.4 0-2.74 0-.93-.33-1.54-.69-1.85 2.23-.25 4.57-1.09 4.57-4.91 0-1.11-.38-2-1.03-2.71.1-.25.45-1.29-.1-2.64 0 0-.84-.27-2.75 1.02-.79-.22-1.65-.33-2.5-.33s-1.71.11-2.5.33C7.59 5.88 6.75 6.15 6.75 6.15c-.55 1.35-.2 2.39-.1 2.64-.65.71-1.03 1.6-1.03 2.71 0 3.81 2.33 4.67 4.55 4.92-.28.25-.54.69-.63 1.34-.57.24-2.04.69-2.91-.83 0 0-.53-.96-1.53-1.03 0 0-.98-.02-.07.6 0 0 .65.31 1.11 1.47 0 0 .59 1.94 3.36 1.34 0 .83 0 1.46 0 1.69 0 .27-.16.58-.66.5C4.87 20.17 2 16.42 2 12A10 10 0 0112 2Z
|
||||
</Geometry>
|
||||
|
||||
<Geometry x:Key="CheckDecagram">
|
||||
M10 17l8-8-1.41-1.42L10 14.17 7.41 11.59 6 13l4 4Zm13-5-2.44 2.78.34 3.68-3.61.82-1.89 3.18L12 21 8.6 22.47 6.71 19.29 3.1 18.47l.34-3.69L1 12 3.44 9.21 3.1 5.53l3.61-.81L8.6 1.54 12 3l3.4-1.46 1.89 3.18 3.61.82-.34 3.68L23 12
|
||||
</Geometry>
|
||||
|
||||
<Geometry x:Key="AlertDecagram">
|
||||
M13 13V7H11v6h2Zm0 4V15H11v2h2m10-5-2.44 2.78.34 3.68-3.61.82-1.89 3.18L12 21 8.6 22.47 6.71 19.29 3.1 18.47l.34-3.69L1 12 3.44 9.21 3.1 5.53l3.61-.81L8.6 1.54 12 3l3.4-1.46 1.89 3.18 3.61.82-.34 3.68L23 12
|
||||
</Geometry>
|
||||
|
||||
<Geometry x:Key="ArrowLeft">
|
||||
M5.05 11.94l5-5v3.99H19l-.03 2.01H10.05v4Z
|
||||
</Geometry>
|
||||
|
||||
<!--<Geometry x:Key="">
|
||||
|
||||
</Geometry>-->
|
||||
</ResourceDictionary>
|
||||
Binary file not shown.
@@ -0,0 +1,354 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<CircleEase EasingMode="EaseInOut" x:Key="BaseAnimationFunction"/>
|
||||
|
||||
<Style TargetType="{x:Type Button}">
|
||||
<Style.Resources>
|
||||
<CornerRadius x:Key="CornerRadius">3 3 3 3</CornerRadius>
|
||||
</Style.Resources>
|
||||
<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="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border CornerRadius="{DynamicResource CornerRadius}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Secondary}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ColoredButton" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="{DynamicResource Primary}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryForeground}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="{DynamicResource Muted}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource MutedForeground}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Margin" Value="0 2 0 2"/>
|
||||
<Setter Property="Background" Value="{DynamicResource Primary}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="IconButton" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border CornerRadius="{DynamicResource CornerRadius}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
Background="{TemplateBinding Background}">
|
||||
<Viewbox HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Path x:Name="Icon" Stretch="Fill" Data="{TemplateBinding Tag}" Fill="{TemplateBinding Foreground}"/>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<CircleEase EasingMode="EaseIn" x:Key="DefaultAnimationFunction"/>
|
||||
|
||||
<Style TargetType="{x:Type ContextMenu}">
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="OverridesDefaultStyle" Value="True"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ContextMenu}">
|
||||
<Border x:Name="Content" CornerRadius="5" Margin="5"
|
||||
Background="{StaticResource Background}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource Border}"
|
||||
Padding="4">
|
||||
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Cycle" />
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="5" ShadowDepth="0" Color="Black" Opacity="0.4"/>
|
||||
</Border.Effect>
|
||||
<Border.RenderTransform>
|
||||
<ScaleTransform ScaleX="0" ScaleY="0"/>
|
||||
</Border.RenderTransform>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<EventTrigger RoutedEvent="Loaded">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Duration="0:0:0.15" Storyboard.TargetName="Content" EasingFunction="{StaticResource DefaultAnimationFunction}"
|
||||
Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)" From="0" To="1"/>
|
||||
<DoubleAnimation Duration="0:0:0.15" Storyboard.TargetName="Content" EasingFunction="{StaticResource DefaultAnimationFunction}"
|
||||
Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)" From="0" To="1"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type MenuItem}">
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="OverridesDefaultStyle" Value="True"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Foreground}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Border}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type MenuItem}">
|
||||
<Border Name="Root" Height="30" Background="Transparent" CornerRadius="4">
|
||||
<ContentPresenter Name="HeaderHost" Margin="10,0,10,0"
|
||||
ContentSource="Header" MinWidth="100"
|
||||
RecognizesAccessKey="True"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
TextOptions.TextRenderingMode="ClearType" TextBlock.FontSize="12" TextBlock.FontWeight="{TemplateBinding FontWeight}" TextBlock.Foreground="{TemplateBinding Foreground}" TextOptions.TextFormattingMode="Display"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Foreground}"/>
|
||||
<Setter TargetName="Root" Property="Background" Value="{DynamicResource Accent}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Background}"/>
|
||||
<Setter TargetName="Root" Property="Background" Value="{DynamicResource Foreground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="LabelCard" TargetType="{x:Type Border}">
|
||||
<Setter Property="Background" Value="{DynamicResource Card}"/>
|
||||
<Setter Property="Opacity" Value="0.9"/>
|
||||
<Setter Property="CornerRadius" Value="10"/>
|
||||
<Setter Property="Padding" Value="12 6"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Label" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Foreground}"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
</Style>
|
||||
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Cursor" Value="Hand"></Setter>
|
||||
<Setter Property="Content" Value=""/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type CheckBox}">
|
||||
<Border x:Name="Border" Height="17" Width="17"
|
||||
CornerRadius="3"
|
||||
Background="{DynamicResource Foreground}" BorderBrush="{DynamicResource Border}"
|
||||
BorderThickness="0">
|
||||
<TextBlock x:Name="Text" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource PrimaryForeground}"></TextBlock>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="False">
|
||||
<Setter TargetName="Border"
|
||||
Property="Background" Value="Transparent"/>
|
||||
<Setter TargetName="Border"
|
||||
Property="BorderThickness" Value="1"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Text"
|
||||
Property="Text" Value="✓"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="{x:Null}">
|
||||
<Setter TargetName="Text"
|
||||
Property="Text" Value="–"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
|
||||
<Style TargetType="TextBox" x:Key="TitledTextBox">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Foreground}" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource MutedForeground}" />
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource MutedForeground}" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type TextBox}">
|
||||
<Border BorderBrush="{StaticResource Border}" Cursor="IBeam"
|
||||
BorderThickness="1" CornerRadius="3">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border BorderBrush="{DynamicResource Border}"
|
||||
BorderThickness="0 0 1 0" IsHitTestVisible="False">
|
||||
<TextBlock Text="{DynamicResource mw_folder_path}"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource MutedForeground}"
|
||||
Padding="10 0" />
|
||||
</Border>
|
||||
<ScrollViewer
|
||||
Grid.Column="1"
|
||||
Margin="5 0"
|
||||
VerticalAlignment="Center"
|
||||
x:Name="PART_ContentHost" />
|
||||
<TextBlock IsHitTestVisible="False"
|
||||
Grid.Column="1"
|
||||
Opacity="0.3"
|
||||
Text="{DynamicResource mw_folder_not_found}"
|
||||
Margin="7 0 5 1"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="Collapsed"
|
||||
x:Name="Placeholder" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="Text" Value="">
|
||||
<Setter TargetName="Placeholder"
|
||||
Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</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 WeModPatcher.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,87 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using WeModPatcher.Models;
|
||||
|
||||
namespace WeModPatcher.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");
|
||||
|
||||
foreach (var folder in Constants.WeModBrandNames)
|
||||
{
|
||||
var weModDir = Path.Combine(localAppDataPath ?? "", folder);
|
||||
if(Directory.Exists(weModDir))
|
||||
{
|
||||
return FindLatestWeMod(weModDir);
|
||||
}
|
||||
}
|
||||
|
||||
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,130 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WeModPatcher.Utils.Win32
|
||||
{
|
||||
public class Shortcut
|
||||
{
|
||||
public class ShortcutParams
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string TargetPath { get; set; }
|
||||
public string Arguments { get; set; }
|
||||
public string WorkingDirectory { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Hotkey { get; set; }
|
||||
public string IconPath { get; set; }
|
||||
};
|
||||
|
||||
private static readonly Type m_type = Type.GetTypeFromProgID("WScript.Shell");
|
||||
private static readonly object m_shell = Activator.CreateInstance(m_type);
|
||||
|
||||
[ComImport, TypeLibType(0x1040), Guid("F935DC23-1CF0-11D0-ADB9-00C04FD58A0B")]
|
||||
private interface IWshShortcut
|
||||
{
|
||||
[DispId(0)]
|
||||
string FullName { [return: MarshalAs(UnmanagedType.BStr)][DispId(0)] get; }
|
||||
[DispId(0x3e8)]
|
||||
string Arguments { [return: MarshalAs(UnmanagedType.BStr)][DispId(0x3e8)] get; [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3e8)] set; }
|
||||
[DispId(0x3e9)]
|
||||
string Description { [return: MarshalAs(UnmanagedType.BStr)][DispId(0x3e9)] get; [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3e9)] set; }
|
||||
[DispId(0x3ea)]
|
||||
string Hotkey { [return: MarshalAs(UnmanagedType.BStr)][DispId(0x3ea)] get; [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3ea)] set; }
|
||||
[DispId(0x3eb)]
|
||||
string IconLocation { [return: MarshalAs(UnmanagedType.BStr)][DispId(0x3eb)] get; [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3eb)] set; }
|
||||
[DispId(0x3ec)]
|
||||
string RelativePath { [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3ec)] set; }
|
||||
[DispId(0x3ed)]
|
||||
string TargetPath { [return: MarshalAs(UnmanagedType.BStr)][DispId(0x3ed)] get; [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3ed)] set; }
|
||||
[DispId(0x3ee)]
|
||||
int WindowStyle { [DispId(0x3ee)] get; [param: In][DispId(0x3ee)] set; }
|
||||
[DispId(0x3ef)]
|
||||
string WorkingDirectory { [return: MarshalAs(UnmanagedType.BStr)][DispId(0x3ef)] get; [param: In, MarshalAs(UnmanagedType.BStr)][DispId(0x3ef)] set; }
|
||||
[TypeLibFunc((short)0x40), DispId(0x7d0)]
|
||||
void Load([In, MarshalAs(UnmanagedType.BStr)] string PathLink);
|
||||
[DispId(0x7d1)]
|
||||
void Save();
|
||||
}
|
||||
|
||||
public static void CreateShortcut(string fileName, string targetPath, string arguments, string workingDirectory, string description, string iconPath)
|
||||
{
|
||||
IWshShortcut shortcut = (IWshShortcut)m_type.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod, null, m_shell, new object[] { fileName });
|
||||
shortcut.Description = description;
|
||||
shortcut.TargetPath = targetPath;
|
||||
shortcut.WorkingDirectory = workingDirectory;
|
||||
shortcut.Arguments = arguments;
|
||||
if (!string.IsNullOrEmpty(iconPath))
|
||||
shortcut.IconLocation = iconPath;
|
||||
shortcut.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<UserControl x:Class="WeModPatcher.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"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Viewbox Width="20" Height="20" VerticalAlignment="Top">
|
||||
<Path Fill="{Binding IconColor}" Data="{Binding IconData}"/>
|
||||
</Viewbox>
|
||||
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="5 0 5 0" TextWrapping="Wrap"
|
||||
FontSize="12" Text="{Binding Text}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace WeModPatcher.View.Controls
|
||||
{
|
||||
public partial class InfoItem : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty IconDataProperty =
|
||||
DependencyProperty.Register(nameof(IconData), typeof(Geometry), typeof(InfoItem));
|
||||
|
||||
public static readonly DependencyProperty IconColorProperty =
|
||||
DependencyProperty.Register(nameof(IconColor), typeof(Brush), typeof(InfoItem));
|
||||
|
||||
public static readonly DependencyProperty TextProperty =
|
||||
DependencyProperty.Register(nameof(Text), typeof(string), typeof(InfoItem));
|
||||
|
||||
public Geometry IconData
|
||||
{
|
||||
get => (Geometry)GetValue(IconDataProperty);
|
||||
set => SetValue(IconDataProperty, value);
|
||||
}
|
||||
|
||||
public Brush IconColor
|
||||
{
|
||||
get => (Brush)GetValue(IconColorProperty);
|
||||
set => SetValue(IconColorProperty, value);
|
||||
}
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => (string)GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
public InfoItem()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContext = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<Grid x:Class="WeModPatcher.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"
|
||||
mc:Ignorable="d"
|
||||
Visibility="Collapsed">
|
||||
<Border x:Name="Splash" Background="Black" CornerRadius="7"
|
||||
Opacity="0.45"
|
||||
MouseLeftButtonDown="HidePopup"/>
|
||||
|
||||
<Border Background="{DynamicResource Background}" d:Margin="0"
|
||||
Margin="0 40 0 40" x:Name="PopupPresenter" Width="Auto" Height="Auto"
|
||||
BorderBrush="{DynamicResource Border}" BorderThickness="1" MinWidth="300"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center" CornerRadius="4" Padding="15 10 10 15">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Button BorderThickness="0" BorderBrush="Transparent"
|
||||
Tag="{StaticResource CloseIcon}"
|
||||
Width="25" Height="25" Padding="8" Background="Transparent"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top" Click="HidePopup"
|
||||
x:Name="cancel">
|
||||
<Button.Style>
|
||||
<Style BasedOn="{StaticResource IconButton}" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource MutedForeground}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource Foreground}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Row="0" x:Name="TitleContainer" Orientation="Horizontal">
|
||||
<TextBlock x:Name="Title" Text="This is title" Foreground="{DynamicResource Foreground}"
|
||||
HorizontalAlignment="Left" FontWeight="Bold" FontSize="16"
|
||||
VerticalAlignment="Bottom"/>
|
||||
</StackPanel>
|
||||
|
||||
<ContentPresenter x:Name="Presenter" Margin="0 20 0 0"
|
||||
Content="{Binding PopupContent}" Grid.Row="2"/>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace WeModPatcher.View.Controls
|
||||
{
|
||||
public partial class PopupHost : Grid
|
||||
{
|
||||
internal Action Closed;
|
||||
|
||||
public static readonly DependencyProperty PopupContentProperty =
|
||||
DependencyProperty.Register("PopupContent", typeof(object), typeof(PopupHost), new PropertyMetadata(null));
|
||||
|
||||
internal readonly SemaphoreSlim OpenedSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
private DoubleAnimation OpeningAnimation;
|
||||
private DoubleAnimation ClosingAnimation;
|
||||
|
||||
|
||||
public bool IsOpen
|
||||
{
|
||||
get => this.Visibility == Visibility.Visible;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
if(OpenedSemaphore.CurrentCount == 0)
|
||||
return;
|
||||
|
||||
|
||||
Visibility = Visibility.Visible;
|
||||
cancel.Focus();
|
||||
PopupPresenter.BeginAnimation(OpacityProperty, OpeningAnimation);
|
||||
OpenedSemaphore.Wait();
|
||||
}
|
||||
else
|
||||
{
|
||||
PopupPresenter.BeginAnimation(OpacityProperty, ClosingAnimation);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public object PopupContent
|
||||
{
|
||||
get => GetValue(PopupContentProperty);
|
||||
set => SetValue(PopupContentProperty, value);
|
||||
}
|
||||
|
||||
private void HidePopup(object sender, EventArgs e)
|
||||
{
|
||||
if (OpenedSemaphore.CurrentCount == 1)
|
||||
return;
|
||||
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
private void OnClosing(object sender, EventArgs e)
|
||||
{
|
||||
if (PopupContent == null)
|
||||
return;
|
||||
|
||||
Visibility = Visibility.Collapsed;
|
||||
Closed?.Invoke();
|
||||
if (PopupContent is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
PopupContent = null;
|
||||
Closed = null;
|
||||
OpenedSemaphore.Release();
|
||||
}
|
||||
|
||||
public PopupHost()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
PreviewKeyDown += (sender, e) =>
|
||||
{
|
||||
if (e.Key != Key.Escape)
|
||||
return;
|
||||
|
||||
HidePopup(null, null);
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
OpeningAnimation = new DoubleAnimation(0, 1, new Duration(TimeSpan.FromSeconds(0.4)))
|
||||
{
|
||||
EasingFunction = App.Current.FindResource("BaseAnimationFunction") as IEasingFunction
|
||||
};
|
||||
OpeningAnimation.Freeze();
|
||||
|
||||
ClosingAnimation = new DoubleAnimation(1, 0, new Duration(TimeSpan.FromSeconds(0.2)));
|
||||
ClosingAnimation.Completed += OnClosing;
|
||||
ClosingAnimation.Freeze();
|
||||
|
||||
this.Splash.DataContext = this;
|
||||
this.PopupPresenter.DataContext = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace WeModPatcher.View.MainWindow
|
||||
{
|
||||
public enum ELogType
|
||||
{
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
Success
|
||||
}
|
||||
public class LogEntry
|
||||
{
|
||||
public ELogType LogType { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<Window x:Class="WeModPatcher.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"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance local:MainWindowVm}"
|
||||
Title="WeMod Patcher"
|
||||
Height="510" MaxHeight="510"
|
||||
Width="780" MaxWidth="780"
|
||||
Opacity="0.97"
|
||||
Background="Transparent"
|
||||
WindowStyle="None"
|
||||
FontFamily="{StaticResource Inter}"
|
||||
AllowsTransparency="True">
|
||||
<Border CornerRadius="7" Background="{DynamicResource Background}"
|
||||
BorderBrush="{DynamicResource Border}"
|
||||
BorderThickness="1" Margin="10">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="15" Direction="-90"
|
||||
RenderingBias="Quality" ShadowDepth="2"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Grid>
|
||||
<Grid Background="Transparent" VerticalAlignment="Top"
|
||||
MouseLeftButtonDown="OnDragMove" Height="55">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="25 0 0 0">
|
||||
<Viewbox VerticalAlignment="Center" Width="32" Height="32">
|
||||
<Path
|
||||
Fill="White" Data="{StaticResource Logo}"/>
|
||||
</Viewbox>
|
||||
<TextBlock Foreground="{DynamicResource Foreground}"
|
||||
FontWeight="SemiBold" Opacity="0.9"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18" Margin="10 0 0 0">
|
||||
<Bold>
|
||||
WeMod Patcher
|
||||
</Bold>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="VersionLabel" VerticalAlignment="Bottom"
|
||||
Opacity="0.7" FontSize="10" Margin="5 0 0 5"
|
||||
Foreground="{DynamicResource Foreground}">
|
||||
v 1.0.0
|
||||
</TextBlock>
|
||||
|
||||
<Button Background="SpringGreen" Foreground="{DynamicResource Muted}"
|
||||
FontWeight="Medium" Padding="20 0" Margin="10 5 20 5"
|
||||
ToolTip="Click to update"
|
||||
Command="{Binding UpdateCommand}"
|
||||
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource ToVisibilityConverter}}"
|
||||
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}"
|
||||
Width="25" Height="25" Padding="6.5"
|
||||
HorizontalAlignment="Right"
|
||||
Click="OnClosing"
|
||||
VerticalAlignment="Center">
|
||||
<Button.Resources>
|
||||
<CornerRadius x:Key="CornerRadius">5 5 5 5</CornerRadius>
|
||||
</Button.Resources>
|
||||
<Button.Style>
|
||||
<Style BasedOn="{StaticResource IconButton}" TargetType="Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Secondary}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource Destructive}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Border Background="{DynamicResource Border}" Height="1" VerticalAlignment="Bottom"></Border>
|
||||
|
||||
</Grid>
|
||||
<Grid Margin="0 55 0 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="50"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Margin="10" Cursor="Hand" Background="Transparent">
|
||||
<TextBox Style="{StaticResource TitledTextBox}"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding WeModInfo.RootDirectory, Mode=OneWay}"
|
||||
VerticalAlignment="Center">
|
||||
</TextBox>
|
||||
<Grid.InputBindings>
|
||||
<MouseBinding Gesture="LeftClick" Command="{Binding SetFolderPathCommand}" />
|
||||
</Grid.InputBindings>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Border Grid.Row="1" BorderBrush="{DynamicResource Border}" BorderThickness="1"
|
||||
Margin="10 0 10 10"
|
||||
CornerRadius="5">
|
||||
<ListBox ItemsSource="{Binding LogList}" SelectionMode="Single"
|
||||
BorderBrush="Transparent" BorderThickness="0"
|
||||
Background="Transparent"
|
||||
x:Name="LogList"
|
||||
Padding="6"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.CanContentScroll="False">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Margin" Value="0 0 0 5"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Card" HorizontalAlignment="Left"
|
||||
Background="#08fc81"
|
||||
BorderBrush="{DynamicResource Border}"
|
||||
Padding="5 3 5 3" CornerRadius="3">
|
||||
<TextBox Text="{Binding Message}"
|
||||
Cursor="IBeam" FontSize="13"
|
||||
Background="Transparent" TextWrapping="Wrap"
|
||||
BorderThickness="0"
|
||||
IsReadOnly="True"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding LogType}" Value="Error">
|
||||
<Setter TargetName="Card" Property="Background" Value="#f04343"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding LogType}" Value="Info">
|
||||
<Setter TargetName="Card" Property="Background" Value="#FFF"></Setter>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding LogType}" Value="Warn">
|
||||
<Setter TargetName="Card" Property="Background" Value="#facc15"></Setter>
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Grid.Row="2" Margin="10 0 10 10">
|
||||
<Grid>
|
||||
<Grid HorizontalAlignment="Right" >
|
||||
<Grid.Style>
|
||||
<Style TargetType="{x:Type Grid}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsPatchEnabled}" Value="False">
|
||||
<Setter Property="Cursor" Value="No"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
<Button Style="{StaticResource ColoredButton}"
|
||||
IsEnabled="{Binding IsPatchEnabled}"
|
||||
FontWeight="Bold" FontSize="16" Width="200"
|
||||
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="{DynamicResource mw_restore}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
|
||||
<DockPanel Grid.Row="2" Margin="10 0 10 10">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
||||
Cursor="Hand"
|
||||
HorizontalAlignment="Left"
|
||||
MouseDown="OpenSourceClicked"
|
||||
Background="Transparent">
|
||||
<Viewbox VerticalAlignment="Center" Width="32" Height="32">
|
||||
<Path
|
||||
Fill="White" Data="{StaticResource GitHub}"/>
|
||||
</Viewbox>
|
||||
<Grid>
|
||||
<TextBlock Margin="8 0 0 0" FontSize="10" Foreground="{DynamicResource AccentForeground}">
|
||||
<Hyperlink Foreground="{DynamicResource AccentForeground}"><Run Text="{DynamicResource mw_source_code}"/></Hyperlink>
|
||||
<LineBreak/>
|
||||
<Run Text="{DynamicResource mw_made_by}"/>
|
||||
|
||||
|
||||
<LineBreak/>
|
||||
<Run Foreground="{DynamicResource MutedForeground}" Text="{DynamicResource mw_star_hint}"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<controls:PopupHost x:Name="PopupHost"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace WeModPatcher.View.MainWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
public static MainWindow Instance;
|
||||
public readonly MainWindowVm ViewModel;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.ViewModel = new MainWindowVm(this);
|
||||
this.DataContext = ViewModel;
|
||||
VersionLabel.Text = Constants.Version.ToString();
|
||||
Instance = this;
|
||||
|
||||
}
|
||||
|
||||
public void OpenPopup(FrameworkElement content, string title = null)
|
||||
{
|
||||
this.PopupHost.PopupContent = content;
|
||||
PopupHost.Title.Text = title;
|
||||
PopupHost.IsOpen = true;
|
||||
}
|
||||
|
||||
private void OnDragMove(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
this.DragMove();
|
||||
}
|
||||
|
||||
private void OnClosing(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
public void ClosePopup()
|
||||
{
|
||||
PopupHost.IsOpen = false;
|
||||
}
|
||||
|
||||
private void OpenSourceClicked(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
System.Diagnostics.Process.Start(Constants.RepositoryUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using WeModPatcher.Core;
|
||||
using WeModPatcher.Models;
|
||||
using WeModPatcher.ReactiveUICore;
|
||||
using WeModPatcher.Utils;
|
||||
using WeModPatcher.View.Popups;
|
||||
using Application = System.Windows.Application;
|
||||
|
||||
namespace WeModPatcher.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 WeModConfig _weModConfig;
|
||||
|
||||
public WeModConfig WeModInfo
|
||||
{
|
||||
get => _weModConfig;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _weModConfig, value);
|
||||
if (value == null) return;
|
||||
|
||||
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);
|
||||
IsPatchEnabled = false;
|
||||
AlreadyPatched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Log("Ready for patching.", ELogType.Info);
|
||||
IsPatchEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isPatchEnabled;
|
||||
|
||||
public bool IsPatchEnabled
|
||||
{
|
||||
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 RelayCommand UpdateCommand { get; }
|
||||
public RelayCommand OpenSettingsCommand { get; }
|
||||
|
||||
private void OnFolderPathSelection(object obj)
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
{
|
||||
dialog.SelectedPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
dialog.Description = "Select the WeMod directory";
|
||||
dialog.ShowNewFolderButton = false;
|
||||
|
||||
if (dialog.ShowDialog() != DialogResult.OK) return;
|
||||
string selectedPath = dialog.SelectedPath;
|
||||
string fileName = Path.GetFileName(selectedPath);
|
||||
|
||||
var info = Extensions.CheckWeModPath(selectedPath);
|
||||
|
||||
if (info != null)
|
||||
{
|
||||
WeModInfo = info;
|
||||
return;
|
||||
}
|
||||
|
||||
LogList.Add(new LogEntry
|
||||
{
|
||||
LogType = ELogType.Error,
|
||||
Message = $"The selected folder '{fileName}' is not a valid WeMod directory."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBackupRestoring(object param)
|
||||
{
|
||||
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))
|
||||
{
|
||||
}
|
||||
|
||||
var proxyDllPath = Path.Combine(WeModInfo.RootDirectory, "version.dll");
|
||||
|
||||
if(File.Exists(proxyDllPath))
|
||||
{
|
||||
File.Delete(proxyDllPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log("Backup file is locked. Please close the WeMod and try again.", ELogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
File.Copy(backupPath, Path.Combine(WeModInfo.RootDirectory, "resources", "app.asar"), true);
|
||||
File.Delete(backupPath);
|
||||
Log("Backup restored successfully.", ELogType.Success);
|
||||
AlreadyPatched = false;
|
||||
IsPatchEnabled = true;
|
||||
}
|
||||
|
||||
private void OnPatching(object param)
|
||||
{
|
||||
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.ClosePopup();
|
||||
IsPatchEnabled = false;
|
||||
await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
new Patcher(WeModInfo, Log, config).Patch();
|
||||
AlreadyPatched = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log($"Failed to patch: {e.Message}", ELogType.Error);
|
||||
IsPatchEnabled = true;
|
||||
}
|
||||
});
|
||||
}), Application.Current.FindResource("pv_popup_title") as string);
|
||||
}
|
||||
|
||||
private void Log(string message, ELogType logType)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
message = $"[{logType.ToString().ToUpper()}] {message}";
|
||||
|
||||
var entry = new LogEntry
|
||||
{
|
||||
LogType = logType,
|
||||
Message = message
|
||||
};
|
||||
LogList.Add(entry);
|
||||
_view.LogList.ScrollIntoView(entry);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUpdate(object param)
|
||||
{
|
||||
MainWindow.Instance.OpenPopup(new UpdatePopup(() =>
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _updater.Update();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log($"Failed to update: {e.Message}", ELogType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("WeModPatcher updated successfully. Restarting...", ELogType.Success);
|
||||
});
|
||||
}), Application.Current.FindResource("up_popup_title") as string);
|
||||
}
|
||||
|
||||
private void OnOpenSettings(object param)
|
||||
{
|
||||
MainWindow.Instance.OpenPopup(new SettingsPopup(), Application.Current.FindResource("settings_title") as string);
|
||||
}
|
||||
|
||||
public MainWindowVm(MainWindow view)
|
||||
{
|
||||
Task.Run(async () => IsUpdateAvailable = await _updater.CheckForUpdates());
|
||||
_view = view;
|
||||
SetFolderPathCommand = new RelayCommand(OnFolderPathSelection);
|
||||
ApplyPatchCommand = new RelayCommand(OnPatching);
|
||||
RestoreBackupCommand = new RelayCommand(OnBackupRestoring);
|
||||
UpdateCommand = new RelayCommand(OnUpdate);
|
||||
OpenSettingsCommand = new RelayCommand(OnOpenSettings);
|
||||
|
||||
WeModInfo = Extensions.FindWeMod();
|
||||
if (WeModInfo == null)
|
||||
{
|
||||
Log("WeMod directory not found.", ELogType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<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">
|
||||
<Grid>
|
||||
<Grid 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="{DynamicResource pv_activate_pro}" />
|
||||
<CheckBox Grid.Row="0" x:Name="ActivateProBox" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="True" />
|
||||
|
||||
<TextBlock Grid.Row="1" VerticalAlignment="Center" Text="{DynamicResource pv_devtools}" />
|
||||
<CheckBox Grid.Row="1" x:Name="DevToolsHotkeyBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
|
||||
<TextBlock Grid.Row="2" VerticalAlignment="Center" Text="{DynamicResource pv_disable_updates}" />
|
||||
<CheckBox Grid.Row="2" x:Name="DisableUpdateBox" HorizontalAlignment="Right" VerticalAlignment="Center" />
|
||||
|
||||
<!--<TextBlock
|
||||
ToolTip="Disable if you want to use older versions separately and manage versions manually via different shortcuts"
|
||||
ToolTipService.InitialShowDelay="300"
|
||||
Grid.Row="3" VerticalAlignment="Center">
|
||||
Apply the patch to new versions <LineBreak /> automatically (hover to see more)
|
||||
</TextBlock>
|
||||
<CheckBox Grid.Row="3" x:Name="AutoUpdates" HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
IsChecked="True" />-->
|
||||
|
||||
<Button Grid.Row="3" Padding="0 5 0 5" Margin="0 15 0 0" Content="{DynamicResource pv_start}"
|
||||
Click="OnPatchButtonClick" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,51 @@
|
||||
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
|
||||
{
|
||||
private readonly Action<PatchConfig> _onApply;
|
||||
|
||||
public PatchVectorsPopup(Action<PatchConfig> onApply)
|
||||
{
|
||||
_onApply = onApply;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnPatchButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true &&
|
||||
DevToolsHotkeyBox.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);
|
||||
}
|
||||
|
||||
_onApply(new PatchConfig
|
||||
{
|
||||
PatchTypes = result,
|
||||
AutoApplyPatches =/* AutoUpdates.IsChecked == true*/ false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<UserControl x:Class="WeModPatcher.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 WeModPatcher.Core;
|
||||
using WeModPatcher.Core.Services;
|
||||
using WeModPatcher.View.MainWindow;
|
||||
|
||||
namespace WeModPatcher.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,20 @@
|
||||
<UserControl x:Class="WeModPatcher.View.Popups.UpdatePopup"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:WeModPatcher.View.Popups"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="Auto" d:DesignWidth="Auto"
|
||||
Background="{DynamicResource Background}"
|
||||
Foreground="{DynamicResource MutedForeground}"
|
||||
FontWeight="Medium"
|
||||
FontSize="13">
|
||||
<StackPanel>
|
||||
|
||||
<TextBlock Foreground="Red" MaxWidth="320" TextAlignment="Center" Text="{DynamicResource up_warning}" TextWrapping="Wrap" />
|
||||
|
||||
<Button Padding="0 5 0 5" Margin="0 15 0 0" Content="{DynamicResource up_update_now}"
|
||||
Click="OnUpdateClick" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WeModPatcher.View.Popups
|
||||
{
|
||||
public partial class UpdatePopup : UserControl
|
||||
{
|
||||
private readonly Action _onUpdate;
|
||||
|
||||
public UpdatePopup(Action onUpdate)
|
||||
{
|
||||
_onUpdate = onUpdate;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnUpdateClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_onUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\packages\ILRepack.2.0.41\build\ILRepack.props" Condition="Exists('..\packages\ILRepack.2.0.41\build\ILRepack.props')" />
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>WeModPatcher</RootNamespace>
|
||||
<AssemblyName>WeModPatcher</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<ApplicationIcon>..\assets\appicon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32bit>false</Prefer32bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DebugType>none</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32bit>false</Prefer32bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject>WeModPatcher.Program</StartupObject>
|
||||
<CMakeSourceDir>..\tools\asar-fuses-bypass</CMakeSourceDir>
|
||||
<CMakeBuildDir>$(CMakeSourceDir)\cmake-build-release</CMakeBuildDir>
|
||||
<ProxyDllPath>$(CMakeBuildDir)\version.dll</ProxyDllPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xaml">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Compile Include="Constants.cs" />
|
||||
<Compile Include="Converters\BaseBooleanConverter.cs" />
|
||||
<Compile Include="Converters\ToVisibilityConverter.cs" />
|
||||
<Compile Include="Core\Patcher.cs" />
|
||||
<Compile Include="Core\PatcherConfig.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\Updater.cs" />
|
||||
<Compile Include="Utils\Win32\Shortcut.cs" />
|
||||
<Compile Include="View\Controls\InfoItem.xaml.cs">
|
||||
<DependentUpon>InfoItem.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="View\Controls\PopupHost.xaml.cs" />
|
||||
<Compile Include="View\MainWindow\Logs.cs" />
|
||||
<Compile Include="View\MainWindow\MainWindow.xaml.cs" />
|
||||
<Compile Include="View\MainWindow\MainWindowVm.cs" />
|
||||
<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" />
|
||||
<Page Include="View\Controls\InfoItem.xaml" />
|
||||
<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">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\assets\appicon.ico">
|
||||
<Link>appicon.ico</Link>
|
||||
</None>
|
||||
<None Include="App.config" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Style\Inter_18pt-Regular.ttf" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AsarSharp\AsarSharp.csproj">
|
||||
<Project>{beaa604a-402a-4387-8903-a53fc913a26e}</Project>
|
||||
<Name>AsarSharp</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(ProxyDllPath)">
|
||||
<LogicalName>proxydll</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\ILRepack.2.0.41\build\ILRepack.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\ILRepack.2.0.41\build\ILRepack.props'))" />
|
||||
</Target>
|
||||
|
||||
<Target Name="EmbedProxyDll" BeforeTargets="BeforeBuild">
|
||||
<Error Text="Proxy DLL not found: $(ProxyDllPath)"
|
||||
Condition="!Exists('$(ProxyDllPath)')" />
|
||||
|
||||
<Message Text="Embedding Proxy DLL as resource from $(ProxyDllPath)"
|
||||
Importance="high" />
|
||||
</Target>
|
||||
|
||||
<Target Name="ILRepack" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PropertyGroup>
|
||||
<ILRepackExe>..\packages\ILRepack.2.0.41\tools\ILRepack.exe</ILRepackExe>
|
||||
<MainAssembly>$(OutputPath)$(AssemblyName).exe</MainAssembly>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyList Include="$(OutputPath)*.dll" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DllList>@(AssemblyList->'%(FullPath)', ' ')</DllList>
|
||||
</PropertyGroup>
|
||||
|
||||
<Exec Command=""$(ILRepackExe)" /allowMultiple /copyattrs /out:"$(OutputPath)$(AssemblyName).exe" "$(MainAssembly)" $(DllList)" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="ILRepack" version="2.0.41" targetFramework="net48" developmentDependency="true" />
|
||||
<package id="Microsoft.Build.Framework" version="15.9.20" targetFramework="net48" />
|
||||
<package id="Microsoft.Build.Utilities.Core" version="15.9.20" targetFramework="net48" />
|
||||
<package id="Microsoft.VisualStudio.Setup.Configuration.Interop" version="1.16.30" targetFramework="net48" developmentDependency="true" />
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
<package id="System.Collections.Immutable" version="1.5.0" targetFramework="net48" />
|
||||
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net48" />
|
||||
</packages>
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeModPatcher", "WeModPatcher\WeModPatcher.csproj", "{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsarSharp", "AsarSharp\AsarSharp.csproj", "{BEAA604A-402A-4387-8903-A53FC913A26E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{106D3E44-ECBB-4EF3-84B2-5FC6BCF77727}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BEAA604A-402A-4387-8903-A53FC913A26E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BEAA604A-402A-4387-8903-A53FC913A26E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BEAA604A-402A-4387-8903-A53FC913A26E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BEAA604A-402A-4387-8903-A53FC913A26E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="170px" height="170px" fill-rule="nonzero"><g transform="translate(17.92,17.92) scale(0.86,0.86)"><g fill="#27272a" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M53.5814,276.83721c-41.10026,0 -74.4186,-33.31834 -74.4186,-74.4186v-148.83721c0,-41.10026 33.31834,-74.4186 74.4186,-74.4186h148.83721c41.10026,0 74.4186,33.31834 74.4186,74.4186v148.83721c0,41.10026 -33.31834,74.4186 -74.4186,74.4186z" id="shape"></path></g><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.12,5.12)"><path d="M47.845,22.185l-20.03,-20.03c-1.543,-1.543 -4.046,-1.553 -5.729,0.002l-19.931,20.028c-1.542,1.542 -1.554,4.045 0,5.727l19.934,19.934c0.772,0.772 1.785,1.16 2.816,1.16c1.026,0 2.07,-0.385 2.91,-1.16l19.933,-19.934c1.605,-1.605 1.648,-4.175 0.097,-5.727zM18,27c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2zM25,34c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2zM25,20c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2zM32,27c-1.105,0 -2,-0.895 -2,-2c0,-1.105 0.895,-2 2,-2c1.105,0 2,0.895 2,2c0,1.105 -0.895,2 -2,2z"></path></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
-48
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WeMod Patcher</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="main-section">
|
||||
<div class="header">
|
||||
<h1>WeMod Patcher</h1>
|
||||
<p>Select the folder with the WeMod version like: “app-X.XX.X” and click patch</p>
|
||||
</div>
|
||||
|
||||
<div class="path-section">
|
||||
<label for="file-path">Folder path:</label>
|
||||
<div class="path-input-container">
|
||||
<input type="text" id="file-path" class="path-input" placeholder="Select file..." readonly>
|
||||
<button class="browse-btn" id="browse-btn">Browse</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="patch-btn" class="patch-btn" disabled>
|
||||
Patch
|
||||
</button>
|
||||
|
||||
<div class="log-section">
|
||||
<div class="log-entry info">Waiting for action...</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div>
|
||||
<span id="version-label"></span>
|
||||
<button id="updateBtn" class="hidden">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<svg id="sourceBtn" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="40" height="40" viewBox="0 0 24 24">
|
||||
<path fill="#ffff" d="M10.9,2.1c-4.6,0.5-8.3,4.2-8.8,8.7c-0.5,4.7,2.2,8.9,6.3,10.5C8.7,21.4,9,21.2,9,20.8v-1.6c0,0-0.4,0.1-0.9,0.1 c-1.4,0-2-1.2-2.1-1.9c-0.1-0.4-0.3-0.7-0.6-1C5.1,16.3,5,16.3,5,16.2C5,16,5.3,16,5.4,16c0.6,0,1.1,0.7,1.3,1c0.5,0.8,1.1,1,1.4,1 c0.4,0,0.7-0.1,0.9-0.2c0.1-0.7,0.4-1.4,1-1.8c-2.3-0.5-4-1.8-4-4c0-1.1,0.5-2.2,1.2-3C7.1,8.8,7,8.3,7,7.6C7,7.2,7,6.6,7.3,6 c0,0,1.4,0,2.8,1.3C10.6,7.1,11.3,7,12,7s1.4,0.1,2,0.3C15.3,6,16.8,6,16.8,6C17,6.6,17,7.2,17,7.6c0,0.8-0.1,1.2-0.2,1.4 c0.7,0.8,1.2,1.8,1.2,3c0,2.2-1.7,3.5-4,4c0.6,0.5,1,1.4,1,2.3v2.6c0,0.3,0.3,0.6,0.7,0.5c3.7-1.5,6.3-5.1,6.3-9.3 C22,6.1,16.9,1.4,10.9,2.1z"></path>
|
||||
</svg>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,134 +0,0 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const Unlocker = require("./unlocker");
|
||||
const GitHubUpdater = require("./updater");
|
||||
|
||||
const packageJsonPath = path.join(__dirname, 'package.json');
|
||||
const packageData = require(packageJsonPath)
|
||||
const updater = new GitHubUpdater(packageData.author, packageData.name);
|
||||
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
resizable: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
|
||||
win.loadFile('index.html');
|
||||
}
|
||||
|
||||
const checkWeModPath = (root) => fs.existsSync(path.join(root, 'WeMod.exe')) &&
|
||||
fs.existsSync(path.join(root, 'resources/app.asar'))
|
||||
|
||||
function log(message, type = 'info') {
|
||||
BrowserWindow.getAllWindows().forEach(win => {
|
||||
win.webContents.send('log', { message, type });
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('select-file', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: process.env.LOCALAPPDATA || path.join(process.env.HOME || process.env.USERPROFILE, 'AppData', 'Local')
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
return {
|
||||
filePath: result.filePaths[0],
|
||||
fileName: path.basename(result.filePaths[0]),
|
||||
valid: checkWeModPath(result.filePaths[0])
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
ipcMain.handle('apply-patch', async (event, path) => {
|
||||
const unlocker = new Unlocker(path, (e) => log(e))
|
||||
await unlocker.start()
|
||||
})
|
||||
|
||||
ipcMain.handle('resolve-default-path', async () => {
|
||||
const defaultDir = path.join(process.env.LOCALAPPDATA || path.join(process.env.HOME || process.env.USERPROFILE, 'AppData', 'Local'), 'WeMod');
|
||||
|
||||
if (!fs.existsSync(defaultDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(defaultDir, {withFileTypes: true});
|
||||
const appFolders = items
|
||||
.filter(item => item.isDirectory() && /^app-\w+/.test(item.name))
|
||||
.map(item => {
|
||||
const folderPath = path.join(defaultDir, item.name);
|
||||
const stats = fs.statSync(folderPath);
|
||||
return {
|
||||
name: item.name,
|
||||
path: folderPath,
|
||||
mtime: stats.mtime,
|
||||
};
|
||||
});
|
||||
|
||||
let appDir = null;
|
||||
appFolders.sort((a, b) => b.mtime - a.mtime);
|
||||
for (const folder of appFolders) {
|
||||
if (checkWeModPath(folder.path)) {
|
||||
appDir = folder.path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return appDir;
|
||||
})
|
||||
|
||||
ipcMain.handle('start-patch', async (event, filePath) => {
|
||||
try {
|
||||
// stub
|
||||
return {
|
||||
success: true,
|
||||
message: 'Patch completed successfully!'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-current-version", () => {
|
||||
return packageData.version
|
||||
})
|
||||
|
||||
|
||||
ipcMain.handle("check-updates", async () => {
|
||||
return await updater.checkForUpdates()
|
||||
})
|
||||
|
||||
ipcMain.on('open-link', () => {
|
||||
require('electron').shell.openExternal("https://github.com/k1tbyte/Wemod-Patcher")
|
||||
})
|
||||
|
||||
ipcMain.on("apply-update", async (event, source) => {
|
||||
try {
|
||||
log("Downloading update ...")
|
||||
const path = await updater.downloadUpdate(source)
|
||||
log("Preparation")
|
||||
updater.applyUpdate(path)
|
||||
} catch (err) {
|
||||
log(err, "error")
|
||||
}
|
||||
})
|
||||
@@ -1,81 +0,0 @@
|
||||
const fs = require("fs");
|
||||
|
||||
function findPatternInBuffer(buffer, bytesRead, signature, mask) {
|
||||
const bufferLength = bytesRead + signature.length - 1;
|
||||
|
||||
for (let i = 0; i <= bytesRead - signature.length; i++) {
|
||||
if (isMatch(buffer, signature, mask, i)) return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
function isMatch(buffer, signature, mask, offset) {
|
||||
for (let i = 0; i < signature.length; i++) {
|
||||
if (mask[i] === "x" && buffer[offset + i] !== signature[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseSignature(signature) {
|
||||
const signatureBytes = [];
|
||||
let mask = "";
|
||||
|
||||
const tokens = signature.split(" ");
|
||||
tokens.forEach((token) => {
|
||||
if (token === "??" || token === "?") {
|
||||
signatureBytes.push(0);
|
||||
mask += "?";
|
||||
} else {
|
||||
signatureBytes.push(parseInt(token, 16));
|
||||
mask += "x";
|
||||
}
|
||||
});
|
||||
|
||||
return { signature: Buffer.from(signatureBytes), mask };
|
||||
}
|
||||
|
||||
|
||||
async function patchBySignature(filePath, functionSignature, patchBytes, patchOffset) {
|
||||
const { signature, mask } = parseSignature(functionSignature);
|
||||
|
||||
const bufferSize = 8192;
|
||||
const buffer = Buffer.alloc(bufferSize + signature.length - 1);
|
||||
|
||||
const fileHandle = await fs.promises.open(filePath, "r+");
|
||||
|
||||
let filePosition = 0;
|
||||
try {
|
||||
while (true) {
|
||||
const { bytesRead } = await fileHandle.read(buffer, 0, bufferSize, filePosition);
|
||||
if (bytesRead === 0) break;
|
||||
|
||||
const matchIndex = findPatternInBuffer(buffer, bytesRead, signature, mask);
|
||||
if (matchIndex !== -1) {
|
||||
const functionStartPosition = filePosition + matchIndex;
|
||||
|
||||
const checkBuffer = Buffer.alloc(patchBytes.length);
|
||||
await fileHandle.read(checkBuffer, 0, patchBytes.length, functionStartPosition + patchOffset);
|
||||
|
||||
if (Buffer.compare(checkBuffer, Buffer.from(patchBytes)) === 0) {
|
||||
return 0; // Memory already patched
|
||||
}
|
||||
|
||||
// Go to patch position
|
||||
await fileHandle.write(Buffer.from(patchBytes), 0, patchBytes.length, functionStartPosition + patchOffset);
|
||||
|
||||
return functionStartPosition; // Return the address of the function start by signature
|
||||
}
|
||||
|
||||
filePosition += bytesRead;
|
||||
buffer.copy(buffer, 0, bufferSize, bufferSize + signature.length - 1);
|
||||
}
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
module.exports = patchBySignature;
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "wemod-patcher",
|
||||
"version": "0.0.1",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder --win portable"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "k1tbyte",
|
||||
"license": "Apache-2.0",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"asar": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^25.1.8"
|
||||
},
|
||||
"build": {
|
||||
"appId": "kitbyte.wemod-patcher",
|
||||
"productName": "WeMod Patcher",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"win": {
|
||||
"icon": "./appicon.ico",
|
||||
"target": [{
|
||||
"target": "portable",
|
||||
"arch": ["x64"]
|
||||
}],
|
||||
"artifactName": "${productName}.exe"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "./appicon.ico",
|
||||
"uninstallerIcon": "./appicon.ico",
|
||||
"installerHeaderIcon": "./appicon.ico"
|
||||
},
|
||||
"compression": "maximum",
|
||||
"removePackageScripts": true,
|
||||
"removePackageKeywords": true,
|
||||
"extraResources": false,
|
||||
"electronLanguages": ["en-US"]
|
||||
}
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
class PatcherUI {
|
||||
constructor() {
|
||||
this.selectedPath = '';
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
|
||||
ipcRenderer.on('log', (event, { message, type }) => {
|
||||
this.addLog(message, type);
|
||||
});
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.filePathInput = document.getElementById('file-path');
|
||||
this.browseBtn = document.getElementById('browse-btn');
|
||||
this.patchBtn = document.getElementById('patch-btn');
|
||||
this.updateBtn = document.getElementById('updateBtn');
|
||||
this.versionLabel = document.getElementById('version-label');
|
||||
this.logSection = document.querySelector('.log-section');
|
||||
document.getElementById(`sourceBtn`).addEventListener('click', () => {
|
||||
ipcRenderer.send("open-link")
|
||||
})
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.browseBtn.addEventListener('click', () => this.selectFile());
|
||||
this.patchBtn.addEventListener('click', () => this.startPatch());
|
||||
}
|
||||
|
||||
setPath(path) {
|
||||
this.filePathInput.value = this.selectedPath = path
|
||||
this.patchBtn.disabled = !path;
|
||||
}
|
||||
|
||||
async selectFile() {
|
||||
try {
|
||||
const result = await ipcRenderer.invoke('select-file');
|
||||
if (!result?.filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!result.valid) {
|
||||
this.addLog(`The folder “${result.filePath}” is not recognized as a Wemod directory`, 'error')
|
||||
return;
|
||||
}
|
||||
|
||||
this.setPath(result.filePath)
|
||||
this.addLog('Folder selected: ' + result.fileName, 'info');
|
||||
} catch (error) {
|
||||
this.addLog('Error selecting file: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
addLog(message, type = 'info') {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${type}`;
|
||||
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||
this.logSection.appendChild(entry);
|
||||
this.logSection.scrollTop = this.logSection.scrollHeight;
|
||||
}
|
||||
|
||||
async startPatch() {
|
||||
if (!this.selectedPath) return;
|
||||
|
||||
this.patchBtn.disabled = true;
|
||||
this.addLog('Starting patch process...', 'info');
|
||||
|
||||
try {
|
||||
await ipcRenderer.invoke(
|
||||
'apply-patch',
|
||||
this.selectedPath
|
||||
);
|
||||
this.addLog("Success", 'success');
|
||||
} catch (error) {
|
||||
this.addLog('Patch failed: ' + error.message, 'error');
|
||||
} finally {
|
||||
this.patchBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
resolveDefault() {
|
||||
|
||||
ipcRenderer.invoke("get-current-version").then((v) =>
|
||||
this.versionLabel.textContent = `Current version: ${v}`);
|
||||
|
||||
ipcRenderer.invoke("resolve-default-path").then(path => {
|
||||
this.addLog(path ? 'The WeMod folder has been found!' :
|
||||
"WeMod folder was not found. You need to specify the path manually", path ? "success" : "warning"
|
||||
);
|
||||
this.setPath(path)
|
||||
})
|
||||
|
||||
ipcRenderer.invoke("check-updates").then(result => {
|
||||
if(!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateBtn.className = ""
|
||||
this.updateBtn.textContent = `Update to ${result.version}`
|
||||
this.updateBtn.addEventListener('click', () => {
|
||||
this.updateBtn.className = "hidden"
|
||||
ipcRenderer.send("apply-update", result);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', async() => {
|
||||
const ui = new PatcherUI();
|
||||
await ui.resolveDefault()
|
||||
});
|
||||
-188
@@ -1,188 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #191a33;
|
||||
color: #beb2b2;
|
||||
padding: 0 50px 0 50px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #21223d;
|
||||
padding: 24px 24px 15px;
|
||||
border: 1px solid #1e2039;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.main-section {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 26px;
|
||||
margin-bottom: 8px;
|
||||
color: #6d67fd;
|
||||
background: #3a3881;
|
||||
border-radius: 10px;
|
||||
padding: 5px 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #beb2b2;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.path-section {
|
||||
background: #26254b;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #1e2039;
|
||||
}
|
||||
|
||||
.path-section label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #f8f6f6;
|
||||
}
|
||||
|
||||
.path-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.path-input {
|
||||
flex: 1;
|
||||
background: #21223d;
|
||||
color: #beb2b2;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #6d67fd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.browse-btn {
|
||||
background: #6d67fd;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #f8f6f6;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.browse-btn:hover {
|
||||
background: #6d67fd;
|
||||
}
|
||||
|
||||
.patch-btn {
|
||||
margin: 15px 0;
|
||||
background: #6d67fd;
|
||||
color: #f8f6f6;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.patch-btn:hover:enabled {
|
||||
scale: 105%;
|
||||
}
|
||||
|
||||
.patch-btn:disabled {
|
||||
background: #3a3881;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.log-section {
|
||||
background: #26254b;
|
||||
color: #beb2b2;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
height: 100%;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #1e2039;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #08fc81;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f04343;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #6d67fd;
|
||||
}
|
||||
|
||||
#sourceBtn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#updateBtn {
|
||||
background: springgreen;
|
||||
color: #1e2039;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
margin-left: 10px;
|
||||
padding: 10px 25px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
#updateBtn:hover {
|
||||
scale: 105%
|
||||
}
|
||||
@@ -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,15 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(asar_fuses_bypass C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
#[[
|
||||
add_executable(asar_fuses_bypass main.c)
|
||||
]]
|
||||
|
||||
set(CMAKE_SHARED_LIBRARY_PREFIX "")
|
||||
set(CMAKE_STATIC_LIBRARY_PREFIX "")
|
||||
|
||||
add_link_options(-static -static-libgcc -static-libstdc++)
|
||||
|
||||
add_library(version SHARED library.c library.def fuses.c)
|
||||
@@ -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,88 @@
|
||||
//
|
||||
// Created by kitbyte on 30.11.2025.
|
||||
//
|
||||
#include <Windows.h>
|
||||
|
||||
extern BOOL disable_asar_integrity(void);
|
||||
|
||||
#ifdef _WIN64
|
||||
#define WRAPPER_GENFUNC(name) \
|
||||
FARPROC orig_##name; \
|
||||
void _##name(); \
|
||||
__asm__( \
|
||||
".global _" #name "\n" \
|
||||
"_" #name ":\n" \
|
||||
" movq orig_" #name "(%rip), %rax\n" \
|
||||
" jmp *%rax\n" \
|
||||
);
|
||||
#else
|
||||
#define WRAPPER_GENFUNC(name) \
|
||||
FARPROC orig_##name; \
|
||||
__declspec(naked) void _##name() \
|
||||
{ \
|
||||
asm("jmp *_orig_"#name); \
|
||||
}
|
||||
#endif
|
||||
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoA)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoByHandle)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoExW)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoExA)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoSizeA)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoSizeExA)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoSizeExW)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoSizeW)
|
||||
WRAPPER_GENFUNC(GetFileVersionInfoW)
|
||||
WRAPPER_GENFUNC(VerFindFileA)
|
||||
WRAPPER_GENFUNC(VerFindFileW)
|
||||
WRAPPER_GENFUNC(VerInstallFileA)
|
||||
WRAPPER_GENFUNC(VerInstallFileW)
|
||||
WRAPPER_GENFUNC(VerLanguageNameA)
|
||||
WRAPPER_GENFUNC(VerLanguageNameW)
|
||||
WRAPPER_GENFUNC(VerQueryValueA)
|
||||
WRAPPER_GENFUNC(VerQueryValueW)
|
||||
|
||||
#define WRAPPER_FUNC(name) orig_##name = GetProcAddress(hOriginalDll, #name);
|
||||
|
||||
void SourceInit()
|
||||
{
|
||||
TCHAR source[MAX_PATH];
|
||||
GetSystemDirectory(source, MAX_PATH);
|
||||
strcat_s(source, sizeof source, "\\version.dll");
|
||||
HMODULE hOriginalDll = LoadLibrary(source);
|
||||
|
||||
WRAPPER_FUNC(GetFileVersionInfoA);
|
||||
WRAPPER_FUNC(GetFileVersionInfoByHandle);
|
||||
WRAPPER_FUNC(GetFileVersionInfoExW);
|
||||
WRAPPER_FUNC(GetFileVersionInfoExA);
|
||||
WRAPPER_FUNC(GetFileVersionInfoSizeA);
|
||||
WRAPPER_FUNC(GetFileVersionInfoSizeExW);
|
||||
WRAPPER_FUNC(GetFileVersionInfoSizeExA);
|
||||
WRAPPER_FUNC(GetFileVersionInfoSizeW);
|
||||
WRAPPER_FUNC(GetFileVersionInfoW);
|
||||
WRAPPER_FUNC(VerFindFileA);
|
||||
WRAPPER_FUNC(VerFindFileW);
|
||||
WRAPPER_FUNC(VerInstallFileA);
|
||||
WRAPPER_FUNC(VerInstallFileW);
|
||||
WRAPPER_FUNC(VerLanguageNameA);
|
||||
WRAPPER_FUNC(VerLanguageNameW);
|
||||
WRAPPER_FUNC(VerQueryValueA);
|
||||
WRAPPER_FUNC(VerQueryValueW);
|
||||
}
|
||||
|
||||
|
||||
void Payload()
|
||||
{
|
||||
disable_asar_integrity();
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HMODULE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
|
||||
{
|
||||
if (fdwReason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
SourceInit();
|
||||
Payload();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
LIBRARY "VERSION"
|
||||
EXPORTS
|
||||
|
||||
GetFileVersionInfoA = _GetFileVersionInfoA
|
||||
GetFileVersionInfoByHandle = _GetFileVersionInfoByHandle
|
||||
GetFileVersionInfoExA = _GetFileVersionInfoExA
|
||||
GetFileVersionInfoExW = _GetFileVersionInfoExW
|
||||
GetFileVersionInfoSizeA = _GetFileVersionInfoSizeA
|
||||
GetFileVersionInfoSizeExA = _GetFileVersionInfoSizeExA
|
||||
GetFileVersionInfoSizeExW = _GetFileVersionInfoSizeExW
|
||||
GetFileVersionInfoSizeW = _GetFileVersionInfoSizeW
|
||||
GetFileVersionInfoW = _GetFileVersionInfoW
|
||||
VerFindFileA = _VerFindFileA
|
||||
VerFindFileW = _VerFindFileW
|
||||
VerInstallFileA = _VerInstallFileA
|
||||
VerInstallFileW = _VerInstallFileW
|
||||
VerLanguageNameA = _VerLanguageNameA
|
||||
VerLanguageNameW = _VerLanguageNameW
|
||||
VerQueryValueA = _VerQueryValueA
|
||||
VerQueryValueW = _VerQueryValueW
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const asar = require('asar');
|
||||
const { execSync } = require("child_process");
|
||||
const patchBySignature = require("./memoryScanner");
|
||||
|
||||
|
||||
const regex = /getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?}\).*?}/g;
|
||||
const asarPatch = "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;})}"
|
||||
const signature = "E8 ?? ?? ?? ?? ?? C0 75 ?? F6 C3 01 74 ?? 48 89 F9 E8 ?? ?? ?? ??"
|
||||
const patchBytes = [0x31]
|
||||
const patchOffset = 0x5
|
||||
// ...
|
||||
// test eax, eax (0x85 for r/m16/32/64)
|
||||
// jnz short loc_1403A4DD2 (Integrity check failed)
|
||||
// call near ptr funk_1445527E0
|
||||
// ...
|
||||
|
||||
class Unlocker {
|
||||
|
||||
constructor(appDir, logger) {
|
||||
this.appDir = appDir;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
#getFetchFieldName(code) {
|
||||
const match = code.match(/this\.#([a-zA-Z_$][0-9a-zA-Z_$]*)\.fetch/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
#patchAsar (unpackedPath) {
|
||||
let items = fs.readdirSync(unpackedPath,{ withFileTypes: true })
|
||||
items = items.filter(item => !item.isDirectory() && /^app-\w+/.test(item.name));
|
||||
if(items.length === 0) {
|
||||
throw new Error(" - No app bundle found");
|
||||
}
|
||||
|
||||
let asarPatchApplied = false;
|
||||
for (const item of items) {
|
||||
const data = fs.readFileSync(path.join(unpackedPath, item.name), { encoding: 'utf8'})
|
||||
|
||||
const matches = data.match(regex)
|
||||
if(!matches) {
|
||||
continue;
|
||||
}
|
||||
if(matches.length > 1) {
|
||||
throw new Error(" - Multiple target functions found. Looks like the version is not supported");
|
||||
}
|
||||
|
||||
const fetchFieldName = this.#getFetchFieldName(matches[0]);
|
||||
if(!fetchFieldName) {
|
||||
throw new Error(" - Fetch field name not found");
|
||||
}
|
||||
|
||||
const patch = asarPatch.replace(/<fetch_field_name>/g, fetchFieldName)
|
||||
|
||||
this.logger(" - Found target function in: " + item.name)
|
||||
this.logger(" - Patching asar...")
|
||||
fs.writeFileSync(path.join(unpackedPath, item.name), data.replace(regex, patch), {encoding: 'utf8'})
|
||||
this.logger(" - Patch applied")
|
||||
asarPatchApplied = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!asarPatchApplied) {
|
||||
throw new Error("Failed to apply patch");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async #patchPE () {
|
||||
this.logger(" - Patching PE...")
|
||||
const pePath = path.join(this.appDir, 'WeMod.exe')
|
||||
const procStart = await patchBySignature(pePath, signature, patchBytes, patchOffset)
|
||||
if(procStart === -1) {
|
||||
throw new Error(" - Signature not found or already patched")
|
||||
}
|
||||
|
||||
this.logger(procStart === 0 ? " - PE already patched" : " - Patch saved")
|
||||
}
|
||||
|
||||
async start () {
|
||||
this.logger(" - Extracting asar...")
|
||||
const asarPath = path.join(this.appDir, 'resources', 'app.asar')
|
||||
const unpackedPath = path.join(this.appDir, 'resources', 'app.asar.unpacked')
|
||||
const backupPath = path.join(this.appDir, 'resources', 'app.asar.backup')
|
||||
|
||||
if(fs.existsSync(backupPath)) {
|
||||
this.logger(" - Backup already exists")
|
||||
} else {
|
||||
execSync(`copy "${asarPath}" "${backupPath}"`, { encoding: "utf-8" });
|
||||
this.logger(" - Backup saved")
|
||||
}
|
||||
|
||||
try {
|
||||
asar.extractAll(asarPath, unpackedPath)
|
||||
} catch(e) {
|
||||
throw new Error("Failed to extract asar: " + e)
|
||||
}
|
||||
|
||||
this.#patchAsar(unpackedPath)
|
||||
|
||||
await asar.createPackageWithOptions(unpackedPath, asarPath,
|
||||
{ unpack: path.join(unpackedPath,"static/unpacked/**") })
|
||||
this.logger(" - Patch saved")
|
||||
|
||||
await this.#patchPE()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Unlocker;
|
||||
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
const { app } = require("electron");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { exec } = require("child_process");
|
||||
|
||||
class GitHubUpdater {
|
||||
constructor(owner, repo, currentVersion) {
|
||||
this.owner = owner;
|
||||
this.repo = repo;
|
||||
this.currentVersion = currentVersion || app.getVersion();
|
||||
this.apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
|
||||
this.downloadDir = path.join(app.getPath("temp"), "update");
|
||||
}
|
||||
|
||||
async checkForUpdates() {
|
||||
const response = await fetch(this.apiUrl, {
|
||||
headers: { "User-Agent": "GitHub-Updater" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`An error occurred while checking for an update: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const release = await response.json();
|
||||
const latestVersion = release.tag_name;
|
||||
const assets = release.assets;
|
||||
|
||||
if (this.currentVersion === latestVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asset = assets.find(a => a.name.endsWith(".exe"));
|
||||
if (!asset) {
|
||||
throw new Error("Unable to find files to update");
|
||||
}
|
||||
|
||||
return {
|
||||
version: latestVersion,
|
||||
url: asset.browser_download_url,
|
||||
name: asset.name,
|
||||
};
|
||||
}
|
||||
|
||||
async downloadUpdate(updateInfo) {
|
||||
const response = await fetch(updateInfo.url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error downloading file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.downloadDir)) {
|
||||
fs.mkdirSync(this.downloadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = path.join(this.downloadDir, updateInfo.name);
|
||||
const fileStream = fs.createWriteStream(filePath);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const downloadStream = response.body.getReader();
|
||||
|
||||
const pump = async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await downloadStream.read();
|
||||
if (done) {
|
||||
fileStream.end();
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
fileStream.write(value);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
reject(new Error(`File write error: ${error.message}`));
|
||||
});
|
||||
|
||||
pump();
|
||||
});
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async applyUpdate(filePath) {
|
||||
try {
|
||||
const currentExecutable = process.env.PORTABLE_EXECUTABLE_FILE;
|
||||
const updateScript = `Start-Sleep -Seconds 3; Copy-Item -Path '${filePath}' -Destination '${currentExecutable}' -Force; Remove-Item -Path '${filePath}' -Force; Start-Sleep -Seconds 2; Start-Process -FilePath '${currentExecutable}';`;
|
||||
|
||||
exec(`start /b "" powershell -WindowStyle Hidden -Command "${updateScript}"`, {
|
||||
windowsHide: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Update failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GitHubUpdater;
|
||||
Reference in New Issue
Block a user