19 Commits
5.7.0 ... 5.7.2

Author SHA1 Message Date
HolographicHat
19f84bdb86 bump version 2025-10-22 06:23:04 +08:00
HolographicHat
9d22fdf6f9 implement WhenFirstSuccessful method for improved task handling 2025-10-22 06:21:22 +08:00
HolographicHat
27fa0d9c84 move srcgen project 2025-10-22 01:46:58 +08:00
HolographicHat
e9baf8f211 [skip ci] update ci 2025-10-18 20:40:18 +08:00
HolographicHat
b960165b7e Merge pull request #144 from Lightczx/master 2025-10-14 16:17:21 +08:00
DismissedLight
a4c2027ada code style 2025-10-14 13:44:51 +08:00
DismissedLight
f49477c49a Generated MinHook.Attach methods 2025-10-14 11:30:58 +08:00
HolographicHat
67c2fb3bda bump lib version 2025-10-10 18:20:13 +08:00
HolographicHat
be3440695d auto enter gate (close #143) 2025-09-16 11:42:49 +08:00
HolographicHat
f8b8a5a9e1 #142 2025-08-30 11:39:41 +08:00
HolographicHat
49f8679996 bump version 2025-08-05 16:46:56 +08:00
HolographicHat
222a26233e mirror 2025-08-05 01:45:02 +08:00
HolographicHat
1130f442fc update native lib 2025-08-05 00:10:13 +08:00
HolographicHat
b80987f574 [skip ci] update ci 2025-08-04 22:38:29 +08:00
HolographicHat
c96395e1a2 call awake in wndhook 2025-08-04 22:27:48 +08:00
HolographicHat
3f42156b20 ReadAtLeast 2025-08-04 16:19:33 +08:00
HolographicHat
45638b7327 update ci 2025-08-03 14:13:08 +08:00
HolographicHat
d514f3b5e7 [skip ci] update ci 2025-08-03 13:51:55 +08:00
HolographicHat
3fe54d908e add wndhook 2025-08-03 13:46:09 +08:00
20 changed files with 446 additions and 155 deletions

View File

@@ -1,6 +1,7 @@
name: .NET Build
on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
@@ -13,7 +14,8 @@ jobs:
run:
working-directory: ./YaeAchievement
steps:
- uses: actions/checkout@v4
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
@@ -27,12 +29,12 @@ jobs:
- name: Upload-AOT
uses: actions/upload-artifact@v4
with:
name: Artifacts-AOT
path: publish\publish
name: aot
path: YaeAchievement\publish\publish
- name: Publish-NoAOT
run: dotnet publish --property:OutputPath=.\naot-publish\ --property:PublishAot=false --property:PublishSingleFile=true --property:PublishTrimmed=true
- name: Upload-NoAOT
uses: actions/upload-artifact@v4
with:
name: Artifacts-NoAOT
path: naot-publish\publish
name: normal
path: YaeAchievement\naot-publish\publish

View File

@@ -2,29 +2,35 @@ name: YaeLib NuGet Publish
on:
workflow_dispatch:
release:
types: [released]
inputs:
confirm_version:
description: 'Version already increased?'
required: true
type: boolean
perform_publish:
description: 'Publish to nuget?'
required: true
default: true
type: boolean
jobs:
publish:
runs-on: windows-latest
defaults:
run:
working-directory: YaeAchievementLib
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Restore NuGet Packages
run: nuget restore lib\YaeAchievementLib.sln
- name: Build
continue-on-error: true
run: msbuild lib\YaeAchievementLib.sln /p:Configuration=Release
- name: Pack
run: nuget pack lib\YaeAchievementLib.nuspec
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Build native library
run: dotnet publish
- name: Publish to NuGet
run: nuget push *.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json
if: ${{ inputs.perform_publish }}
run: nuget push bin\Release\*.nupkg ${{ secrets.NUGET_API_KEY }} -src https://api.nuget.org/v3/index.json
- name: Upload nuget package
uses: actions/upload-artifact@v4
with:
name: nupkg
path: YaeAchievementLib\bin\Release\*.nupkg

View File

@@ -0,0 +1,104 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace YaeAchievement.SourceGeneration;
[Generator(LanguageNames.CSharp)]
public sealed class MinHookAttachGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider<ImmutableArray<AttachInfo>> provider = context.SyntaxProvider.CreateSyntaxProvider(Filter, Transform).Collect();
context.RegisterSourceOutput(provider, Generate);
}
private static bool Filter(SyntaxNode node, CancellationToken token)
{
return node is InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.Text: "MinHook" },
Name.Identifier.Text: "Attach"
}
};
}
private static AttachInfo Transform(GeneratorSyntaxContext context, CancellationToken token)
{
InvocationExpressionSyntax invocation = (InvocationExpressionSyntax)context.Node;
SeparatedSyntaxList<ArgumentSyntax> args = invocation.ArgumentList.Arguments;
if (args.Count is not 3)
{
return null;
}
string type = context.SemanticModel.GetTypeInfo(args[0].Expression).Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
if (string.IsNullOrEmpty(type))
{
return null;
}
return new()
{
MinimallyQualifiedType = type,
};
}
private static void Generate(SourceProductionContext context, ImmutableArray<AttachInfo> infoArray)
{
CompilationUnitSyntax unit = CompilationUnit()
.WithMembers(List<MemberDeclarationSyntax>(
[
FileScopedNamespaceDeclaration(ParseName("Yae.Utilities")),
ClassDeclaration("MinHook")
.WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword)))
.WithMembers(List(GenerateMethods(infoArray)))
]));
context.AddSource("MinHook.Attach.g.cs", unit.NormalizeWhitespace().ToFullString());
}
private static IEnumerable<MemberDeclarationSyntax> GenerateMethods(ImmutableArray<AttachInfo> infoArray)
{
foreach (AttachInfo info in infoArray)
{
TypeSyntax type = ParseTypeName(info.MinimallyQualifiedType);
yield return MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("Attach"))
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.UnsafeKeyword)))
.WithParameterList(ParameterList(SeparatedList(
[
Parameter(Identifier("origin")).WithType(type),
Parameter(Identifier("handler")).WithType(type),
Parameter(Identifier("trampoline")).WithType(type).WithModifiers(TokenList(Token(SyntaxKind.OutKeyword)))
])))
.WithBody(Block(List<StatementSyntax>(
[
ExpressionStatement(InvocationExpression(IdentifierName("Attach"))
.WithArgumentList(ArgumentList(SeparatedList(
[
Argument(CastExpression(IdentifierName("nint"), IdentifierName("origin"))),
Argument(CastExpression(IdentifierName("nint"), IdentifierName("handler"))),
Argument(DeclarationExpression(IdentifierName("nint"), SingleVariableDesignation(Identifier("trampoline1"))))
.WithRefKindKeyword(Token(SyntaxKind.OutKeyword))
])))),
ExpressionStatement(AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentifierName("trampoline"),
CastExpression(type, IdentifierName("trampoline1"))))
])));
}
}
private record AttachInfo
{
public required string MinimallyQualifiedType { get; init; }
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
<Solution>
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" Type="Classic C#" />
<Project Path="YaeAchievement\YaeAchievement.csproj" Type="Classic C#" />
<Project Path="YaeAchievement.SourceGeneration/YaeAchievement.SourceGeneration.csproj" />
<Project Path="YaeAchievementLib\YaeAchievementLib.csproj" />
<Project Path="YaeAchievement\YaeAchievement.csproj" />
</Solution>

View File

@@ -2,5 +2,5 @@
"$schema": "https://aka.ms/CsWin32.schema.json",
"className": "Native",
"allowMarshaling": false,
"public": true
"public": false
}

View File

@@ -1,28 +1,30 @@
CloseClipboard
CreateProcess
CreateRemoteThread
EmptyClipboard
GetConsoleMode
GetDC
GetDeviceCaps
GetModuleHandle
GetProcAddress
GetStdHandle
// kernel32
GlobalLock
OpenProcess
GetStdHandle
GlobalUnlock
OpenClipboard
ResumeThread
SetClipboardData
Module32Next
Module32First
CreateProcess
LoadLibraryEx
VirtualFreeEx
VirtualAllocEx
GetProcAddress
GetConsoleMode
SetConsoleMode
TerminateProcess
VirtualAllocEx
VirtualFreeEx
WaitForSingleObject
CreateRemoteThread
WriteProcessMemory
WaitForSingleObject
GetCurrentConsoleFontEx
CreateToolhelp32Snapshot
OpenProcess
// psapi
GetModuleFileNameEx
LoadLibraryEx
// user32
OpenClipboard
CloseClipboard
EmptyClipboard
SetClipboardData

View File

@@ -7,8 +7,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFramework>net9.0-windows</TargetFramework>
<FileVersion>5.7.0</FileVersion>
<AssemblyVersion>5.7.0</AssemblyVersion>
<FileVersion>5.7.2</FileVersion>
<AssemblyVersion>5.7.2</AssemblyVersion>
<ApplicationIcon>res\icon.ico</ApplicationIcon>
<ApplicationManifest>res\app.manifest</ApplicationManifest>
</PropertyGroup>
@@ -24,7 +24,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- [Update to 3.31.0 breaks AOT build](https://github.com/protocolbuffers/protobuf/issues/21824) -->
<PackageReference Include="Google.Protobuf" Version="3.30.2" />
<PackageReference Include="Google.Protobuf" Version="3.30.2"/>
<PackageReference Include="Grpc.Tools" Version="2.72.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -21,6 +21,10 @@ message MethodRvaConfig {
uint32 do_cmd = 1;
uint32 to_uint16 = 2;
uint32 update_normal_prop = 3;
uint32 new_string = 4;
uint32 find_game_object = 5;
uint32 event_system_update = 6;
uint32 simulate_pointer_click = 7;
}
message NativeLibConfig {

View File

@@ -2,6 +2,12 @@ syntax = "proto3";
option csharp_namespace = "Proto";
message CdnFileInfo {
uint32 size = 1;
uint32 hash = 2;
repeated string urls = 4;
}
message UpdateInfo {
uint32 version_code = 1;
string version_name = 2;
@@ -10,4 +16,5 @@ message UpdateInfo {
bool force_update = 5;
bool enable_lib_download = 6;
bool enable_auto_update = 7;
map<string, CdnFileInfo> cdn_files = 8;
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Spectre.Console;
@@ -23,12 +22,7 @@ public static partial class AppConfig {
} else {
GamePath = ReadGamePathFromProcess();
}
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(GamePath);
if (stream.Read(buffer) == buffer.Length) {
var hash = Convert.ToHexString(MD5.HashData(buffer));
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{hash}"));
}
CacheFile.Write("genshin_impact_game_path_v2", Encoding.UTF8.GetBytes($"{GamePath}\u1145{Utils.GetGameHash(GamePath)}"));
SentrySdk.AddBreadcrumb(GamePath.EndsWith("YuanShen.exe") ? "CN" : "OS", "GamePath");
return;
static bool TryReadGamePathFromCache([NotNullWhen(true)] out string? path) {
@@ -38,9 +32,7 @@ public static partial class AppConfig {
return false;
}
var cacheData = cacheFile.Content.ToStringUtf8().Split("\u1145");
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(cacheData[0]);
if (stream.Read(buffer) != buffer.Length || Convert.ToHexString(MD5.HashData(buffer)) != cacheData[1]) {
if (Utils.GetGameHash(cacheData[0]) != uint.Parse(cacheData[1])) {
return false;
}
path = cacheData[0];

View File

@@ -18,8 +18,8 @@ public static class GlobalVars {
public static readonly string CachePath = Path.Combine(DataPath, "cache");
public static readonly string LibFilePath = Path.Combine(DataPath, "YaeAchievement.dll");
public const uint AppVersionCode = 240;
public const string AppVersionName = "5.7";
public const uint AppVersionCode = 242;
public const string AppVersionName = "5.7.2";
public const string PipeName = "YaeAchievementPipe";

View File

@@ -7,8 +7,6 @@ using static YaeAchievement.Utils;
namespace YaeAchievement;
// TODO: WndHook
internal static class Program {
public static async Task Main(string[] args) {
@@ -47,10 +45,10 @@ internal static class Program {
Environment.Exit(-1);
}
await CheckUpdate(ToBooleanOrDefault(args.GetOrNull(2)));
await CheckUpdate(ToBooleanOrDefault(args.ElementAtOrDefault(2)));
AppConfig.Load(args.GetOrNull(0) ?? "auto");
Export.ExportTo = ToIntOrDefault(args.GetOrNull(1), 114514);
AppConfig.Load(args.ElementAtOrDefault(0) ?? "auto");
Export.ExportTo = ToIntOrDefault(args.ElementAtOrDefault(1), 114514);
AchievementAllDataNotify? data = null;
try {

View File

@@ -2,6 +2,7 @@
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using Windows.Win32.System.LibraryLoader;
using Windows.Win32.System.Threading;
using Spectre.Console;
@@ -13,7 +14,7 @@ using static Windows.Win32.System.Memory.VIRTUAL_FREE_TYPE;
namespace YaeAchievement.Utilities;
public sealed unsafe class GameProcess {
internal sealed unsafe class GameProcess {
public uint Id { get; }
@@ -73,7 +74,29 @@ public sealed unsafe class GameProcess {
if (Native.WaitForSingleObject(hThread, 2000) == 0) {
Native.VirtualFreeEx(Handle, lpLibPath, 0, MEM_RELEASE);
}
var libHandle = Native.LoadLibraryEx(libPath, LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES);
// Get lib base address in target process
byte* baseAddress = null;
using (var hSnap = Native.CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, Id)) {
if (hSnap.IsInvalid) {
throw new Win32Exception { Data = { { "api", "CreateToolhelp32Snapshot" } } };
}
var moduleEntry = new MODULEENTRY32 {
dwSize = (uint) sizeof(MODULEENTRY32)
};
if (Native.Module32First(hSnap, ref moduleEntry)) {
do {
if (new string((sbyte*) &moduleEntry.szExePath._0) == libPath) {
baseAddress = moduleEntry.modBaseAddr;
break;
}
} while (Native.Module32Next(hSnap, ref moduleEntry));
}
}
if (baseAddress == null) {
throw new InvalidOperationException("No matching module found in target process.");
}
//
using var libHandle = Native.LoadLibraryEx(libPath, LOAD_LIBRARY_FLAGS.DONT_RESOLVE_DLL_REFERENCES);
if (libHandle.IsInvalid) {
throw new Win32Exception { Data = { { "api", "LoadLibraryEx" } } };
}
@@ -81,7 +104,9 @@ public sealed unsafe class GameProcess {
if (libMainProc.IsNull) {
throw new Win32Exception { Data = { { "api", "GetProcAddress" } } };
}
var lpStartAddress2 = (delegate*unmanaged[Stdcall]<void*, uint>) libMainProc.Value; // THREAD_START_ROUTINE
var libMainProcRVA = libMainProc.Value - libHandle.DangerousGetHandle();
var lpStartAddress2 = (delegate*unmanaged[Stdcall]<void*, uint>) (baseAddress + libMainProcRVA); // THREAD_START_ROUTINE
//
var hThread2 = Native.CreateRemoteThread(Handle, null, 0, lpStartAddress2, null, 0);
if (hThread2.IsNull) {
throw new Win32Exception { Data = { { "api", "CreateRemoteThread2" } } };

View File

@@ -1,9 +1,11 @@
using System.ComponentModel;
using System.Globalization;
using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -28,30 +30,68 @@ public static class Utils {
public static async Task<byte[]> GetBucketFile(string path, bool useCache = true) {
var transaction = SentrySdk.StartTransaction(path, "bucket.get");
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
SentrySdk.ConfigureScope(static (scope, transaction) => scope.Transaction = transaction, transaction);
var cacheKey = useCache ? path : null;
//
if (_updateInfo?.CdnFiles.TryGetValue(path, out var cdnFile) == true) {
try {
var data = await cdnFile.Urls
.Select(url => GetFileFromCdn(url, cacheKey, cdnFile.Hash, cdnFile.Size))
.WhenFirstSuccessful()
.Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException or InvalidDataException) {}
}
//
try {
var data = await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache);
var data = await WhenFirstSuccessful([
GetFileReal($"https://rin.holohat.work/{path}", cacheKey),
GetFileReal($"https://ena-rin.holohat.work//{path}", cacheKey),
GetFileReal($"https://cn-cd-1259389942.file.myqcloud.com/{path}", cacheKey)
]).Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
AnsiConsole.WriteLine(App.NetworkError, e.Message);
}
try {
var data = await Task.WhenAny(
GetFile("https://rin.holohat.work/{0}", path, useCache),
GetFile("https://cn-cd-1259389942.file.myqcloud.com/{0}", path, useCache)
).Unwrap();
transaction.Finish();
return data;
} catch (Exception ex) when (ex is HttpRequestException or SocketException or TaskCanceledException) {
transaction.Finish();
AnsiConsole.WriteLine(App.NetworkError, ex.Message);
Environment.Exit(-1);
}
transaction.Finish();
Environment.Exit(-1);
throw new UnreachableException();
static async Task<byte[]> GetFile(string baseUrl, string objectKey, bool useCache) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, string.Format(baseUrl, objectKey));
static async Task<byte[]> GetFileFromCdn(string url, string? cacheKey, uint hash, uint size) {
var data = await GetFileReal(url, cacheKey);
if (data.Length != size || Crc32.Compute(data) != hash) {
throw new InvalidDataException();
}
if (data.Length > 44 && Unsafe.As<byte, uint>(ref data[0]) == 0x38464947) { // GIF8
var seed = Unsafe.As<byte, uint>(ref data[44]) ^ 0x01919810;
var hush = Unsafe.As<byte, uint>(ref data[48]) - 0x32123432; //          
var span = data.AsSpan()[52..];
Span<byte> xorTable = stackalloc byte[4096];
new Random((int) seed).NextBytes(xorTable);
for (var i = 0; i < span.Length; i++) {
span[i] ^= xorTable[i % 4096];
}
using var dataStream = new MemoryStream();
unsafe {
fixed (byte* p = span) {
var cmpStream = new UnmanagedMemoryStream(p, span.Length);
using var decompressor = new BrotliStream(cmpStream, CompressionMode.Decompress);
// ReSharper disable once MethodHasAsyncOverload
decompressor.CopyTo(dataStream);
}
}
data = dataStream.ToArray();
if (Crc32.Compute(data) != hush) {
throw new InvalidDataException();
}
}
return data;
}
static async Task<byte[]> GetFileReal(string url, string? cacheKey) {
using var reqwest = new HttpRequestMessage(HttpMethod.Get, url);
CacheItem? cache = null;
if (useCache && CacheFile.TryRead(objectKey, out cache)) {
if (cacheKey != null && CacheFile.TryRead(cacheKey, out cache)) {
reqwest.Headers.TryAddWithoutValidation("If-None-Match", $"{cache.Etag}");
}
using var response = await CHttpClient.SendAsync(reqwest);
@@ -60,18 +100,14 @@ public static class Utils {
}
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
if (useCache) {
if (cacheKey != null) {
var etag = response.Headers.ETag!.Tag;
CacheFile.Write(objectKey, bytes, etag);
CacheFile.Write(cacheKey, bytes, etag);
}
return bytes;
}
}
public static T? GetOrNull<T>(this T[] array, uint index) where T : class {
return array.Length > index ? array[index] : null;
}
public static int ToIntOrDefault(string? value, int defaultValue = 0) {
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
}
@@ -96,7 +132,7 @@ public static class Utils {
}
// ReSharper disable once NotAccessedField.Local
private static UpdateInfo _updateInfo = null!;
private static UpdateInfo? _updateInfo;
public static Task StartSpinnerAsync(string status, Func<StatusContext, Task> func) {
return AnsiConsole.Status().Spinner(Spinner.Known.SimpleDotsScrolling).StartAsync(status, func);
@@ -109,7 +145,7 @@ public static class Utils {
public static async Task CheckUpdate(bool useLocalLib) {
try {
var versionData = await StartSpinnerAsync(App.UpdateChecking, _ => GetBucketFile("schicksal/version"));
var versionInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
var versionInfo = _updateInfo = UpdateInfo.Parser.ParseFrom(versionData)!;
if (GlobalVars.AppVersionCode < versionInfo.VersionCode) {
AnsiConsole.WriteLine(App.UpdateNewVersion, GlobalVars.AppVersionName, versionInfo.VersionName);
AnsiConsole.WriteLine(App.UpdateDescription, versionInfo.Description);
@@ -136,7 +172,6 @@ public static class Utils {
var data = await GetBucketFile("schicksal/lic.dll");
await File.WriteAllBytesAsync(GlobalVars.LibFilePath, data); // 要求重启电脑
}
_updateInfo = versionInfo;
} catch (IOException e) when ((uint) e.HResult == 0x80070020) { // ERROR_SHARING_VIOLATION
// IO_SharingViolation_File
// The process cannot access the file '{0}' because it is being used by another process.
@@ -146,17 +181,14 @@ public static class Utils {
}
// ReSharper disable once UnusedMethodReturnValue.Global
public static bool ShellOpen(string path, string? args = null) {
public static bool ShellOpen(string path, string args = "") {
try {
var startInfo = new ProcessStartInfo {
FileName = path,
UseShellExecute = true
};
if (args != null) {
startInfo.Arguments = args;
}
return new Process {
StartInfo = startInfo
StartInfo = new ProcessStartInfo {
FileName = path,
UseShellExecute = true,
Arguments = args
}
}.Start();
} catch (Exception) {
return false;
@@ -230,11 +262,16 @@ public static class Utils {
writer.Write(methodRva.DoCmd);
writer.Write(methodRva.ToUint16);
writer.Write(methodRva.UpdateNormalProp);
writer.Write(methodRva.NewString);
writer.Write(methodRva.FindGameObject);
writer.Write(methodRva.EventSystemUpdate);
writer.Write(methodRva.SimulatePointerClick);
break;
case 0xFE:
_proc!.ResumeMainThread();
break;
case 0xFF:
writer.Write(true);
_isUnexpectedExit = false;
onFinish();
return;
@@ -259,11 +296,15 @@ public static class Utils {
AnsiConsole.WriteLine(App.GameLoading, _proc.Id);
}
private static uint GetGameHash(string exePath) {
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(exePath);
_ = stream.Read(buffer);
return Crc32.Compute(buffer);
public static uint GetGameHash(string exePath) {
try {
Span<byte> buffer = stackalloc byte[0x10000];
using var stream = File.OpenRead(exePath);
_ = stream.ReadAtLeast(buffer, 0x10000, false);
return Crc32.Compute(buffer);
} catch (IOException) {
return 0xFFFFFFFF;
}
}
internal static unsafe void SetQuickEditMode(bool enable) {
@@ -289,4 +330,26 @@ public static class Utils {
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US"); // todo: use better way like auto set console font etc.
}
}
// https://stackoverflow.com/a/76953892
private static async Task<Task<TResult>> WhenFirstSuccessful<TResult>(this IEnumerable<Task<TResult>> tasks) {
var cts = new CancellationTokenSource();
Task<TResult>? selectedTask = null;
var continuations = tasks
.TakeWhile(_ => !cts.IsCancellationRequested)
.Select(task => {
return task.ContinueWith(t => {
if (t.IsCompletedSuccessfully) {
if (Interlocked.CompareExchange(ref selectedTask, t, null) is null) {
cts.Cancel();
}
}
}, cts.Token, TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
});
var whenAll = Task.WhenAll(continuations);
try {
await whenAll.ConfigureAwait(false);
} catch when (whenAll.IsCanceled) { /* ignore */ }
return selectedTask!;
}
}

View File

@@ -25,20 +25,47 @@
</ItemGroup>
<ItemGroup>
<DirectPInvoke Include="NTDLL"/>
<DirectPInvoke Include="USER32"/>
<DirectPInvoke Include="KERNEL32"/>
<DirectPInvoke Include="libMinHook.x64"/>
<NativeLibrary Include="lib\libMinHook.x64.lib"/>
<DirectPInvoke Include="NTDLL" />
<DirectPInvoke Include="USER32" />
<DirectPInvoke Include="KERNEL32" />
<DirectPInvoke Include="libMinHook.x64" />
<NativeLibrary Include="lib\libMinHook.x64.lib" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute"/>
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute" />
</ItemGroup>
<ItemGroup>
<Using Include="System.Diagnostics"/>
<Using Include="System.Diagnostics.CodeAnalysis"/>
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.CodeAnalysis" />
</ItemGroup>
<PropertyGroup>
<PackageId>Yae.Lib</PackageId>
<Version>5.4.3</Version>
<Authors>HoloHat</Authors>
<DevelopmentDependency>true</DevelopmentDependency>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<RepositoryUrl>https://github.com/HolographicHat/Yae</RepositoryUrl>
<Description>Yae Lib</Description>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<ItemGroup>
<None Include="$(PublishDir)\$(TargetName)$(NativeBinaryExt)" Pack="true" PackagePath="runtimes\win-x64\native" Visible="false" />
</ItemGroup>
<ItemGroup>
<ProjectReference OutputItemType="Analyzer" Include="..\YaeAchievement.SourceGeneration\YaeAchievement.SourceGeneration.csproj">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</ProjectReference>
</ItemGroup>
<Target Name="GenerateNuGetPackage" AfterTargets="CopyNativeBinary">
<Exec Command="dotnet pack --no-build --nologo" UseUtf8Encoding="Always" EchoOff="true" />
</Target>
</Project>

View File

@@ -7,8 +7,13 @@ namespace Yae;
internal static unsafe class Application {
private static bool _initialized;
[UnmanagedCallersOnly(EntryPoint = "YaeMain")]
private static uint Awake(nint hModule) {
if (Interlocked.Exchange(ref _initialized, true)) {
return 1;
}
Native.RegisterUnhandledExceptionHandler();
Log.UseConsoleOutput();
Log.Trace("~");
@@ -24,9 +29,16 @@ internal static unsafe class Application {
MinHook.Attach(GameMethod.DoCmd, &OnDoCmd, out _doCmd);
MinHook.Attach(GameMethod.ToUInt16, &OnToUInt16, out _toUInt16);
MinHook.Attach(GameMethod.UpdateNormalProp, &OnUpdateNormalProp, out _updateNormalProp);
MinHook.Attach(GameMethod.EventSystemUpdate, &OnEventSystemUpdate, out _eventSystemUpdate);
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "YaeWndHook")]
private static nint WndHook(int nCode, nint wParam, nint lParam) {
((delegate*unmanaged<nint, uint>) &Awake)(0);
return User32.CallNextHookEx(0, nCode, wParam, lParam);
}
#region RecvPacket
private static delegate*unmanaged<byte*, int, ushort> _toUInt16;
@@ -131,4 +143,24 @@ internal static unsafe class Application {
#endregion
#region EnterGate
private static long _lastTryEnterTime;
private static delegate*unmanaged<nint, void> _eventSystemUpdate;
[UnmanagedCallersOnly]
public static void OnEventSystemUpdate(nint @this) {
_eventSystemUpdate(@this);
if (Environment.TickCount64 - _lastTryEnterTime > 200) {
var obj = GameMethod.FindGameObject(GameMethod.NewString("BtnStart"u8.AsPointer()));
if (obj != 0 && GameMethod.SimulatePointerClick(@this, obj)) {
MinHook.Detach((nint) GameMethod.EventSystemUpdate);
}
_lastTryEnterTime = Environment.TickCount64;
}
}
#endregion
}

View File

@@ -19,6 +19,14 @@ internal static unsafe class GameMethod {
public static delegate*unmanaged<nint, int, double, double, int, void> UpdateNormalProp { get; set; }
public static delegate*unmanaged<nint, nint> NewString { get; set; }
public static delegate*unmanaged<nint, nint> FindGameObject { get; set; }
public static delegate*unmanaged<nint, void> EventSystemUpdate { get; set; }
public static delegate*unmanaged<nint, nint, bool> SimulatePointerClick { get; set; }
}
internal static class Goshujin {
@@ -26,8 +34,10 @@ internal static class Goshujin {
private static NamedPipeClientStream _pipeStream = null!;
private static BinaryReader _pipeReader = null!;
private static BinaryWriter _pipeWriter = null!;
private static Lock _lock = null!;
public static void Init(string pipeName = "YaeAchievementPipe") {
_lock = new Lock();
_pipeStream = new NamedPipeClientStream(pipeName);
_pipeReader = new BinaryReader(_pipeStream);
_pipeWriter = new BinaryWriter(_pipeStream);
@@ -36,26 +46,32 @@ internal static class Goshujin {
}
public static void PushAchievementData(Span<byte> data) {
_pipeWriter.Write((byte) 1);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_achievementDataPushed = true;
ExitIfFinished();
using (_lock.EnterScope()) {
_pipeWriter.Write((byte) 1);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_achievementDataPushed = true;
ExitIfFinished();
}
}
public static void PushStoreData(Span<byte> data) {
_pipeWriter.Write((byte) 2);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_storeDataPushed = true;
ExitIfFinished();
using (_lock.EnterScope()) {
_pipeWriter.Write((byte) 2);
_pipeWriter.Write(data.Length);
_pipeWriter.Write(data);
_storeDataPushed = true;
ExitIfFinished();
}
}
public static void PushPlayerProp(int type, double value) {
_pipeWriter.Write((byte) 3);
_pipeWriter.Write(type);
_pipeWriter.Write(value);
ExitIfFinished();
using (_lock.EnterScope()) {
_pipeWriter.Write((byte) 3);
_pipeWriter.Write(type);
_pipeWriter.Write(value);
ExitIfFinished();
}
}
public static void LoadCmdTable() {
@@ -69,6 +85,10 @@ internal static class Goshujin {
GameMethod.DoCmd = (delegate*unmanaged<int, void*, int, int>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.ToUInt16 = (delegate*unmanaged<byte*, int, ushort>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.UpdateNormalProp = (delegate*unmanaged<nint, int, double, double, int, void>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.NewString = (delegate*unmanaged<nint, nint>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.FindGameObject = (delegate*unmanaged<nint, nint>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.EventSystemUpdate = (delegate*unmanaged<nint, void>) Native.RVAToVA(_pipeReader.ReadUInt32());
GameMethod.SimulatePointerClick = (delegate*unmanaged<nint, nint, bool>) Native.RVAToVA(_pipeReader.ReadUInt32());
}
public static void ResumeMainThread() {
@@ -82,6 +102,7 @@ internal static class Goshujin {
private static void ExitIfFinished() {
if (_storeDataPushed && _achievementDataPushed && Application.RequiredPlayerProperties.Count == 0) {
_pipeWriter.Write((byte) 0xFF);
_pipeReader.ReadBoolean();
Environment.Exit(0);
}
}

View File

@@ -93,6 +93,9 @@ internal static unsafe class Native {
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint AsPointer(this ReadOnlySpan<byte> span) => *(nint*) Unsafe.AsPointer(ref span);
}
internal static partial class MinHook {
@@ -146,6 +149,7 @@ internal static partial class MinHook {
/// Uninitialize the MinHook library. You must call this function EXACTLY ONCE at the end of your program.
/// </summary>
[LibraryImport("libMinHook.x64", EntryPoint = "MH_Uninitialize")]
// ReSharper disable once UnusedMember.Local
private static partial uint MinHookUninitialize();
static MinHook() {
@@ -155,30 +159,6 @@ internal static partial class MinHook {
}
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<byte*, int, ushort> origin, delegate*unmanaged<byte*, int, ushort> handler, out delegate*unmanaged<byte*, int, ushort> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<byte*, int, ushort>) trampoline1;
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<nint, int, double, double, int, void> origin, delegate*unmanaged<nint, int, double, double, int, void> handler, out delegate*unmanaged<nint, int, double, double, int, void> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<nint, int, double, double, int, void>) trampoline1;
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<nint, nint, uint, void> origin, delegate*unmanaged<nint, nint, uint, void> handler, out delegate*unmanaged<nint, nint, uint, void> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<nint, nint, uint, void>) trampoline1;
}
// todo: auto gen
public static unsafe void Attach(delegate*unmanaged<int, void*, int, int> origin, delegate*unmanaged<int, void*, int, int> handler, out delegate*unmanaged<int, void*, int, int> trampoline) {
Attach((nint) origin, (nint) handler, out var trampoline1);
trampoline = (delegate*unmanaged<int, void*, int, int>) trampoline1;
}
public static void Attach(nint origin, nint handler, out nint trampoline) {
uint result;
if ((result = MinHookCreate(origin, handler, out trampoline)) != 0) {

View File

@@ -43,6 +43,9 @@ internal static unsafe partial class Kernel32 {
[return:MarshalAs(UnmanagedType.I4)]
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
[LibraryImport("KERNEL32.dll", SetLastError = true)]
internal static partial nint CreateThread(nint lpThreadAttributes, nint dwStackSize, delegate*unmanaged<nint, uint> lpStartAddress, nint lpParameter, uint dwCreationFlags, uint* lpThreadId);
}
@@ -65,4 +68,7 @@ internal static unsafe partial class User32 {
[LibraryImport("USER32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial int MessageBoxW(nint hWnd, string text, string caption, uint uType);
[LibraryImport("USER32.dll")]
internal static partial nint CallNextHookEx(nint hhk, int nCode, nint wParam, nint lParam);
}