9 Commits
5.7.0 ... 5.7.1

Author SHA1 Message Date
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
14 changed files with 183 additions and 97 deletions

View File

@@ -13,7 +13,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 +28,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

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

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.1</FileVersion>
<AssemblyVersion>5.7.1</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

@@ -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 = 241;
public const string AppVersionName = "5.7.1";
public const string PipeName = "YaeAchievementPipe";

View File

@@ -47,10 +47,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

@@ -13,7 +13,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; }

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,65 @@ 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);
try {
var data = await GetFile("https://api.qhy04.com/hutaocdn/download?filename={0}", path, useCache);
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
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 tasks = cdnFile.Urls.Select(url => GetFileFromCdn(url, cacheKey, cdnFile.Hash, cdnFile.Size));
var data = await Task.WhenAny(tasks).Unwrap();
transaction.Finish();
return data;
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException or InvalidDataException) {}
}
//
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)
GetFileReal($"https://rin.holohat.work/{path}", cacheKey),
GetFileReal($"https://cn-cd-1259389942.file.myqcloud.com/{path}", cacheKey)
).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);
} catch (Exception e) when (e is SocketException or TaskCanceledException or HttpRequestException) {
AnsiConsole.WriteLine(App.NetworkError, e.Message);
}
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 +97,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 +129,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 +142,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 +169,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 +178,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;
@@ -235,6 +264,7 @@ public static class Utils {
_proc!.ResumeMainThread();
break;
case 0xFF:
writer.Write(true);
_isUnexpectedExit = false;
onFinish();
return;
@@ -259,11 +289,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) {

View File

@@ -41,4 +41,24 @@
<Using Include="System.Diagnostics.CodeAnalysis"/>
</ItemGroup>
<PropertyGroup>
<PackageId>Yae.Lib</PackageId>
<Version>5.4.2</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>
<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("~");
@@ -27,6 +32,12 @@ internal static unsafe class Application {
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;

View File

@@ -26,8 +26,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 +38,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() {
@@ -82,6 +90,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

@@ -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);
}