mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-29 02:01:19 +00:00
electron removed
This commit is contained in:
@@ -128,3 +128,10 @@ dist
|
|||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
|
||||||
|
./WeModPatcher/obj/
|
||||||
|
./WeModPatcher/bin/
|
||||||
|
./AsarSharp/obj/
|
||||||
|
./AsarSharp/bin/
|
||||||
|
.idea
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
StartupUri="/View/MainWindow/MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<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,9 @@
|
|||||||
|
namespace WeModPatcher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for App.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class App
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace WeModPatcher
|
||||||
|
{
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string RepositoryUrl = "https://github.com/k1tbyte/Wemod-Patcher";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 @@
|
|||||||
|
<Window x:Class="WeModPatcher.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"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="MainWindow" Height="350" Width="525">
|
||||||
|
<Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace WeModPatcher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for MainWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace WeModPatcher.Models
|
||||||
|
{
|
||||||
|
|
||||||
|
public enum EPatchType
|
||||||
|
{
|
||||||
|
ActivatePro = 1,
|
||||||
|
DisableUpdates = 2,
|
||||||
|
DisableTelemetry = 4
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.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.ReactiveCore
|
||||||
|
{
|
||||||
|
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.ReactiveCore
|
||||||
|
{
|
||||||
|
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.ReactiveCore
|
||||||
|
{
|
||||||
|
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,18 @@
|
|||||||
|
<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>
|
||||||
|
</ResourceDictionary>
|
||||||
Binary file not shown.
@@ -0,0 +1,245 @@
|
|||||||
|
<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">
|
||||||
|
<TextBlock Text="{TemplateBinding Uid}"
|
||||||
|
VerticalAlignment="Center" Cursor="Hand"
|
||||||
|
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="{TemplateBinding Tag}"
|
||||||
|
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>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AsarSharp;
|
||||||
|
using WeModPatcher.Models;
|
||||||
|
using WeModPatcher.View.MainWindow;
|
||||||
|
|
||||||
|
namespace WeModPatcher.Utils
|
||||||
|
{
|
||||||
|
public class Patcher
|
||||||
|
{
|
||||||
|
private class PatchEntry
|
||||||
|
{
|
||||||
|
public Regex Target { get; set; }
|
||||||
|
public string Patch { get; set; }
|
||||||
|
public bool Applied { get; set; }
|
||||||
|
public bool SingleMatch { get; set; } = true;
|
||||||
|
public bool DynamicFieldResolve { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Dictionary<EPatchType, PatchEntry> Patches = new Dictionary<EPatchType, PatchEntry>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
EPatchType.ActivatePro,
|
||||||
|
new PatchEntry
|
||||||
|
{
|
||||||
|
DynamicFieldResolve = true,
|
||||||
|
Target = new Regex(@"getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?\}\)\}", RegexOptions.Singleline),
|
||||||
|
Patch = "getUserAccount(){return this.#<fetch_field_name>.fetch({endpoint:\"/v3/account\",method:\"GET\",name:\"/v3/account\",collectMetrics:0}).then(response=>{response.subscription={period:\"yearly\",state:\"active\"};response.flags=78;return response;})}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EPatchType.DisableUpdates,
|
||||||
|
new PatchEntry
|
||||||
|
{
|
||||||
|
Target = new Regex(@"registerHandler\(""ACTION_CHECK_FOR_UPDATE"".*?\)\)\)\)", RegexOptions.Singleline),
|
||||||
|
Patch = "registerHandler(\"ACTION_CHECK_FOR_UPDATE\",(e=>expectUpdateFeedUrl(e,(e=>null)))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
private readonly string _weModRootFolder;
|
||||||
|
private readonly Action<string, ELogType> _logger;
|
||||||
|
private readonly HashSet<EPatchType> _config;
|
||||||
|
private readonly string _asarPath;
|
||||||
|
private readonly string _backupPath;
|
||||||
|
private readonly string _unpackedPath;
|
||||||
|
private int _sumOfPatches = 0;
|
||||||
|
|
||||||
|
public Patcher(string weModRootFolder, Action<string, ELogType> logger, HashSet<EPatchType> config)
|
||||||
|
{
|
||||||
|
_weModRootFolder = weModRootFolder;
|
||||||
|
_logger = logger;
|
||||||
|
_config = config;
|
||||||
|
|
||||||
|
_asarPath = Path.Combine(weModRootFolder, "resources", "app.asar");
|
||||||
|
_unpackedPath = Path.Combine(weModRootFolder, "resources", "app.asar.unpacked");
|
||||||
|
_backupPath = Path.Combine(weModRootFolder, "resources", "app.asar.backup");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFetchFieldName(string targetFunction)
|
||||||
|
{
|
||||||
|
var fetchMatch = Regex.Match(targetFunction, @"return\s+this\.#(\w+)\.fetch");
|
||||||
|
return fetchMatch.Success ? fetchMatch.Groups[1].Value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyJsPatch(string fileName, string js, PatchEntry patch, EPatchType patchType)
|
||||||
|
{
|
||||||
|
if (patch.Applied)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = patch.Target.Matches(js);
|
||||||
|
if (matches.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(matches.Count > 1 && patch.SingleMatch)
|
||||||
|
{
|
||||||
|
throw new Exception(
|
||||||
|
$"[PATCHER] [{patchType}] Patch failed. Multiple target functions found. Looks like the version is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.DynamicFieldResolve)
|
||||||
|
{
|
||||||
|
string fetchFieldName = GetFetchFieldName(matches[0].Value);
|
||||||
|
if (string.IsNullOrEmpty(fetchFieldName))
|
||||||
|
{
|
||||||
|
throw new Exception($"[PATCHER] [{patchType}] Fetch field name not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
patch.Patch = patch.Patch.Replace("<fetch_field_name>", fetchFieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger($"[PATCHER] [{patchType}] Found target function in: " + Path.GetFileName(fileName), ELogType.Info);
|
||||||
|
|
||||||
|
|
||||||
|
File.WriteAllText(fileName, patch.Target.Replace(js, patch.Patch));
|
||||||
|
_logger($"[PATCHER] [{patchType}] Patch applied", ELogType.Success);
|
||||||
|
patch.Applied = true;
|
||||||
|
_sumOfPatches -= (int)patchType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PatchAsar()
|
||||||
|
{
|
||||||
|
var items = Directory.EnumerateFiles(_unpackedPath)
|
||||||
|
.Where(file => !Directory.Exists(file) && Regex.IsMatch(Path.GetFileName(file), @"^app-\w+|index\.js"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!items.Any())
|
||||||
|
{
|
||||||
|
throw new Exception("[PATCHER] No app bundle found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestedPatches = _config.ToList();
|
||||||
|
requestedPatches.ForEach(patch => _sumOfPatches += (int)patch);
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (_sumOfPatches <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string data = File.ReadAllText(item);
|
||||||
|
foreach (var entry in requestedPatches)
|
||||||
|
{
|
||||||
|
ApplyJsPatch(item, data, Patches[entry], entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PatchPE()
|
||||||
|
{
|
||||||
|
_logger("[PATCHER] Patching PE...", ELogType.Info);
|
||||||
|
var pePath = Path.Combine(_weModRootFolder, "WeMod.exe");
|
||||||
|
var patchResult = await PatternScanner.PatchBySignature(pePath, PatchSignature, PatchBytes, PatchOffset);
|
||||||
|
if(patchResult == -1)
|
||||||
|
{
|
||||||
|
_logger("[PATCHER] Failed to patch PE", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_logger(patchResult == 0 ? "[PATCHER] PE already patched!" : "[PATCHER] PE patched successfully!", ELogType.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Patch()
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
_logger("[PATCHER] app.asar not found!", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger("[PATCHER] Extracting app.asar...", ELogType.Info);
|
||||||
|
AsarExtractor.ExtractAll(_asarPath, _unpackedPath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger($"[PATCHER] Failed to unpack app.asar: {e.Message}", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchAsar();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new AsarCreator(_unpackedPath, _asarPath, new CreateOptions
|
||||||
|
{
|
||||||
|
Unpack = new Regex(@"^static\\unpacked.*$")
|
||||||
|
}).CreatePackageWithOptions();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger($"[PATCHER] Failed to pack app.asar: {e.Message}", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PatchPE();
|
||||||
|
|
||||||
|
_logger("[PATCHER] Done!", ELogType.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WeModPatcher.Utils
|
||||||
|
{
|
||||||
|
public class PatternScanner
|
||||||
|
{
|
||||||
|
public static int FindPatternInBuffer(byte[] buffer, int bytesRead, byte[] signature, string mask)
|
||||||
|
{
|
||||||
|
int bufferLength = bytesRead + signature.Length - 1;
|
||||||
|
|
||||||
|
for (int i = 0; i <= bytesRead - signature.Length; i++)
|
||||||
|
{
|
||||||
|
if (IsMatch(buffer, signature, mask, i))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMatch(byte[] buffer, byte[] signature, string mask, int offset)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < signature.Length; i++)
|
||||||
|
{
|
||||||
|
if (mask[i] == 'x' && buffer[offset + i] != signature[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (byte[] signature, string mask) ParseSignature(string signature)
|
||||||
|
{
|
||||||
|
var signatureBytes = new List<byte>();
|
||||||
|
var mask = new StringBuilder();
|
||||||
|
|
||||||
|
var tokens = signature.Split(' ');
|
||||||
|
foreach (var token in tokens)
|
||||||
|
{
|
||||||
|
if (token == "??" || token == "?")
|
||||||
|
{
|
||||||
|
signatureBytes.Add(0);
|
||||||
|
mask.Append('?');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
signatureBytes.Add(Convert.ToByte(token, 16));
|
||||||
|
mask.Append('x');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (signatureBytes.ToArray(), mask.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> PatchBySignature(string filePath, string functionSignature, byte[] patchBytes, int patchOffset)
|
||||||
|
{
|
||||||
|
var (signature, mask) = ParseSignature(functionSignature);
|
||||||
|
const int bufferSize = 8192;
|
||||||
|
var buffer = new byte[bufferSize + signature.Length - 1];
|
||||||
|
|
||||||
|
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
|
||||||
|
{
|
||||||
|
int filePosition = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int bytesRead = await fileStream.ReadAsync(buffer, 0, bufferSize);
|
||||||
|
if (bytesRead == 0) break;
|
||||||
|
|
||||||
|
int matchIndex = FindPatternInBuffer(buffer, bytesRead, signature, mask);
|
||||||
|
if (matchIndex != -1)
|
||||||
|
{
|
||||||
|
int functionStartPosition = filePosition + matchIndex;
|
||||||
|
|
||||||
|
var checkBuffer = new byte[patchBytes.Length];
|
||||||
|
fileStream.Seek(functionStartPosition + patchOffset, SeekOrigin.Begin);
|
||||||
|
await fileStream.ReadAsync(checkBuffer, 0, patchBytes.Length);
|
||||||
|
|
||||||
|
if (checkBuffer.SequenceEqual(patchBytes))
|
||||||
|
{
|
||||||
|
return 0; // Memory already patched
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to patch position
|
||||||
|
fileStream.Seek(functionStartPosition + patchOffset, SeekOrigin.Begin);
|
||||||
|
await fileStream.WriteAsync(patchBytes, 0, patchBytes.Length);
|
||||||
|
|
||||||
|
return functionStartPosition; // Return the address of the function start by signature
|
||||||
|
}
|
||||||
|
|
||||||
|
filePosition += bytesRead;
|
||||||
|
Array.Copy(buffer, bufferSize, buffer, 0, signature.Length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<TextBlock x:Name="Title" Text="This is title" Foreground="{DynamicResource Foreground}"
|
||||||
|
HorizontalAlignment="Left" FontWeight="Bold" FontSize="16"
|
||||||
|
VerticalAlignment="Bottom"/>
|
||||||
|
|
||||||
|
<ContentPresenter x:Name="Presenter" Margin="0 20 0 0"
|
||||||
|
Content="{Binding PopupContent}" Grid.Row="2"/>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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();
|
||||||
|
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,186 @@
|
|||||||
|
<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>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<TextBox Style="{StaticResource TitledTextBox}"
|
||||||
|
Uid="Folder path" Margin="10" IsReadOnly="True"
|
||||||
|
Cursor="Hand"
|
||||||
|
Text="{Binding WeModPath}"
|
||||||
|
VerticalAlignment="Center" Tag="Folder not found">
|
||||||
|
<TextBox.InputBindings>
|
||||||
|
<MouseBinding Gesture="LeftClick" Command="{Binding SetFolderPathCommand}" />
|
||||||
|
</TextBox.InputBindings>
|
||||||
|
</TextBox>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" HorizontalAlignment="Right" Margin="10 0 10 10">
|
||||||
|
<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}">Patch</Button>
|
||||||
|
</Grid>
|
||||||
|
<Button Grid.Row="2"
|
||||||
|
Margin="10 0 10 10" HorizontalAlignment="Right"
|
||||||
|
Command="{Binding RestoreBackupCommand }"
|
||||||
|
FontWeight="Bold" FontSize="16" Width="200"
|
||||||
|
Style="{StaticResource ColoredButton}"
|
||||||
|
Visibility="{Binding AlreadyPatched, Converter={StaticResource ToVisibilityConverter}}"
|
||||||
|
Content="Restore"/>
|
||||||
|
|
||||||
|
<DockPanel Grid.Row="2" Margin="10 0 10 10">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
||||||
|
Cursor="Hand"
|
||||||
|
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}">Source code </Hyperlink>
|
||||||
|
<LineBreak/>
|
||||||
|
<Run>Made with ❤️ by k1tbyte</Run>
|
||||||
|
|
||||||
|
|
||||||
|
<LineBreak/>
|
||||||
|
<Run Foreground="{DynamicResource MutedForeground}">Put a star if you found this helpful ;)</Run>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<controls:PopupHost x:Name="PopupHost"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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 MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.DataContext = new MainWindowVm();
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenPopup(object 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,206 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using AsarSharp;
|
||||||
|
using WeModPatcher.ReactiveCore;
|
||||||
|
using WeModPatcher.Utils;
|
||||||
|
using WeModPatcher.View.Popups;
|
||||||
|
using Application = System.Windows.Application;
|
||||||
|
|
||||||
|
namespace WeModPatcher.View.MainWindow
|
||||||
|
{
|
||||||
|
|
||||||
|
public class MainWindowVm : ObservableObject
|
||||||
|
{
|
||||||
|
public ObservableCollection<LogEntry> LogList { get; } = new ObservableCollection<LogEntry>();
|
||||||
|
|
||||||
|
private string _weModPath;
|
||||||
|
|
||||||
|
public string WeModPath
|
||||||
|
{
|
||||||
|
get => _weModPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _weModPath, value);
|
||||||
|
if (value == null) return;
|
||||||
|
|
||||||
|
Log($"WeMod directory found at '{_weModPath}'", ELogType.Success);
|
||||||
|
if (File.Exists(Path.Combine(_weModPath, "resources", "app.asar.backup")))
|
||||||
|
{
|
||||||
|
Log("WeMod 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 = false;
|
||||||
|
|
||||||
|
public bool IsPatchEnabled
|
||||||
|
{
|
||||||
|
get => _isPatchEnabled;
|
||||||
|
set => SetProperty(ref _isPatchEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _alreadyPatched = false;
|
||||||
|
public bool AlreadyPatched
|
||||||
|
{
|
||||||
|
get => _alreadyPatched;
|
||||||
|
set => SetProperty(ref _alreadyPatched, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RelayCommand SetFolderPathCommand { get; }
|
||||||
|
public RelayCommand ApplyPatchCommand { get; }
|
||||||
|
public RelayCommand RestoreBackupCommand { get; }
|
||||||
|
|
||||||
|
private bool CheckWeModPath(string root)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return File.Exists(Path.Combine(root, "WeMod.exe")) &&
|
||||||
|
File.Exists(Path.Combine(root, "resources", "app.asar"));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FindWeModDirectory()
|
||||||
|
{
|
||||||
|
string localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||||
|
|
||||||
|
string defaultDir = Path.Combine(localAppDataPath, "WeMod");
|
||||||
|
|
||||||
|
if (!Directory.Exists(defaultDir))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var appFolders = Directory.EnumerateDirectories(defaultDir)
|
||||||
|
.Select(folderPath => new DirectoryInfo(folderPath))
|
||||||
|
.Where(dirInfo => Regex.IsMatch(dirInfo.Name, @"^app-\w+"))
|
||||||
|
.Select(dirInfo => new
|
||||||
|
{
|
||||||
|
Name = dirInfo.Name,
|
||||||
|
Path = dirInfo.FullName,
|
||||||
|
LastModified = dirInfo.LastWriteTime
|
||||||
|
})
|
||||||
|
.OrderByDescending(item => item.LastModified)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return (
|
||||||
|
from folder
|
||||||
|
in appFolders
|
||||||
|
where CheckWeModPath(folder.Path)
|
||||||
|
select folder.Path
|
||||||
|
).FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (CheckWeModPath(selectedPath))
|
||||||
|
{
|
||||||
|
WeModPath = selectedPath;
|
||||||
|
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(WeModPath, "resources", "app.asar.backup");
|
||||||
|
if (!File.Exists(backupPath))
|
||||||
|
{
|
||||||
|
Log("Backup not found. Please dont delete it manually", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (File.Open(backupPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Log("Backup file is locked. Please close the WeMod and try again.", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(backupPath, Path.Combine(WeModPath, "resources", "app.asar"), true);
|
||||||
|
Log("Backup restored successfully.", ELogType.Success);
|
||||||
|
AlreadyPatched = false;
|
||||||
|
IsPatchEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPatching(object param)
|
||||||
|
{
|
||||||
|
if (WeModPath == 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(() => new Patcher(WeModPath, Log, config).Patch());
|
||||||
|
IsPatchEnabled = true;
|
||||||
|
}), "What are we gonna patch?");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Log(string message, ELogType logType)
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
message = $"[{logType.ToString().ToUpper()}] {message}";
|
||||||
|
|
||||||
|
LogList.Add(new LogEntry
|
||||||
|
{
|
||||||
|
LogType = logType,
|
||||||
|
Message = message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public MainWindowVm()
|
||||||
|
{
|
||||||
|
SetFolderPathCommand = new RelayCommand(OnFolderPathSelection);
|
||||||
|
ApplyPatchCommand = new RelayCommand(OnPatching);
|
||||||
|
RestoreBackupCommand = new RelayCommand(OnBackupRestoring);
|
||||||
|
|
||||||
|
WeModPath = FindWeModDirectory();
|
||||||
|
if (WeModPath == null)
|
||||||
|
{
|
||||||
|
Log("WeMod directory not found.", ELogType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<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"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="300" d:DesignWidth="300"
|
||||||
|
Foreground="{DynamicResource MutedForeground}"
|
||||||
|
FontWeight="Medium"
|
||||||
|
FontSize="13">
|
||||||
|
<Grid Margin="0 0 5 0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="27"/>
|
||||||
|
<RowDefinition Height="27"/>
|
||||||
|
<RowDefinition Height="27"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" VerticalAlignment="Center" Text="Activate WeMod Pro"/>
|
||||||
|
<CheckBox Grid.Row="0" x:Name="ActivateProBox" HorizontalAlignment="Right" VerticalAlignment="Center" IsChecked="True"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" VerticalAlignment="Center" Text="Disable telemetry"/>
|
||||||
|
<CheckBox Grid.Row="1" x:Name="DisableTelemetryBox" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" VerticalAlignment="Center" Text="Disable updates"/>
|
||||||
|
<CheckBox Grid.Row="2" x:Name="DisableUpdateBox" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<Button Grid.Row="3" Padding="0 5 0 5" Margin="0 15 0 0" Content="Continue"
|
||||||
|
Click="ButtonBase_OnClick"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using WeModPatcher.Models;
|
||||||
|
|
||||||
|
namespace WeModPatcher.View.Popups
|
||||||
|
{
|
||||||
|
public partial class PatchVectorsPopup : UserControl
|
||||||
|
{
|
||||||
|
private readonly Action<HashSet<EPatchType>> _onApply;
|
||||||
|
|
||||||
|
public PatchVectorsPopup(Action<HashSet<EPatchType>> onApply)
|
||||||
|
{
|
||||||
|
_onApply = onApply;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if(ActivateProBox.IsChecked != true && DisableUpdateBox.IsChecked != true && DisableTelemetryBox.IsChecked != true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new HashSet<EPatchType>();
|
||||||
|
if (ActivateProBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
result.Add(EPatchType.ActivatePro);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DisableUpdateBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
result.Add(EPatchType.DisableUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onApply(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="4.0" 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>{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>
|
||||||
|
</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="System"/>
|
||||||
|
<Reference Include="System.Core"/>
|
||||||
|
<Reference Include="System.Data"/>
|
||||||
|
<Reference Include="System.Windows.Forms" />
|
||||||
|
<Reference Include="System.Xml"/>
|
||||||
|
<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="Models\PatchConfig.cs" />
|
||||||
|
<Compile Include="ReactiveCore\AsyncRelayCommand.cs" />
|
||||||
|
<Compile Include="ReactiveCore\ObservableObject.cs" />
|
||||||
|
<Compile Include="ReactiveCore\RelayCommand.cs" />
|
||||||
|
<Compile Include="Utils\Patcher.cs" />
|
||||||
|
<Compile Include="Utils\PatternScanner.cs" />
|
||||||
|
<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>
|
||||||
|
<Page Include="MainWindow.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Compile Include="App.xaml.cs">
|
||||||
|
<DependentUpon>App.xaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="MainWindow.xaml.cs">
|
||||||
|
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Page Include="Style\ColorScheme.xaml" />
|
||||||
|
<Page Include="Style\Icons.xaml" />
|
||||||
|
<Page Include="Style\Styles.xaml" />
|
||||||
|
<Page Include="View\Controls\PopupHost.xaml" />
|
||||||
|
<Page Include="View\MainWindow\MainWindow.xaml" />
|
||||||
|
<Page Include="View\Popups\PatchVectorsPopup.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="App.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>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"/>
|
||||||
|
</Project>
|
||||||
@@ -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
|
||||||
-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%
|
|
||||||
}
|
|
||||||
-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