mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
[SaveData] Implement save data memory2 exports (#297)
* [SaveData] Implement save data memory2 exports Astro Bot calls sceSaveDataSetupSaveDataMemory2 during boot and asserts and null-writes when it fails, so the missing import surfaces as a named crash. This implements setup plus the companion get, set, and sync operations that make it useful. The store is one zero-filled file per user and title at sce_sdmemory/memory.dat under the save root, readiness is the backing file's existence, and get, set, and sync return MEMORY_NOT_READY before setup. Struct offsets follow the publicly documented homebrew savedata headers. * [SaveData] Write setup result before mutating the memory backing file
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -14,6 +15,7 @@ public static class SaveDataExports
|
||||
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
||||
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||
private const int OrbisSaveDataErrorMemoryNotReady = unchecked((int)0x809F0012);
|
||||
private const int SaveDataTitleIdSize = 10;
|
||||
private const int SaveDataDirNameSize = 32;
|
||||
private const int SaveDataParamSize = 0x530;
|
||||
@@ -29,7 +31,10 @@ public static class SaveDataExports
|
||||
private const uint MountModeCreate = 1u << 2;
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
private const int MountResultSize = 0x40;
|
||||
// Emulator guard against corrupt or misread sizes, not a platform limit.
|
||||
private const ulong SaveDataMemoryMaxSize = 64UL * 1024 * 1024;
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly object _memoryGate = new();
|
||||
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||
private static string? _titleId;
|
||||
|
||||
@@ -472,6 +477,19 @@ public static class SaveDataExports
|
||||
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||
|
||||
private static string ResolveSaveDataMemoryPath(int userId) =>
|
||||
Path.Combine(ResolveTitleSaveRoot(userId, ResolveConfiguredTitleId()), "sce_sdmemory", "memory.dat");
|
||||
|
||||
private static bool TryReadMemoryData(
|
||||
CpuContext ctx, ulong address, out ulong buffer, out ulong size, out ulong offset)
|
||||
{
|
||||
size = 0;
|
||||
offset = 0;
|
||||
return ctx.TryReadUInt64(address, out buffer) &&
|
||||
ctx.TryReadUInt64(address + 0x08, out size) &&
|
||||
ctx.TryReadUInt64(address + 0x10, out offset);
|
||||
}
|
||||
|
||||
private static string ResolveSaveDataRoot()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
@@ -670,4 +688,197 @@ public static class SaveDataExports
|
||||
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
// Save data memory: a small per-user blob titles read and write without
|
||||
// mounting anything, backed by one zero-filled file per user and title.
|
||||
[SysAbiExport(
|
||||
Nid = "oQySEUfgXRA",
|
||||
ExportName = "sceSaveDataSetupSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSetupSaveDataMemory2(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, paramAddress + 0x04, out var userId) ||
|
||||
!ctx.TryReadUInt64(paramAddress + 0x08, out var memorySize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || memorySize == 0 || memorySize > SaveDataMemoryMaxSize)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = ResolveSaveDataMemoryPath(userId);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
var backing = new FileInfo(path);
|
||||
var existedSize = backing.Exists ? (ulong)backing.Length : 0;
|
||||
|
||||
// The result write comes first so a faulted result pointer
|
||||
// cannot leave created or grown setup state behind.
|
||||
if (resultAddress != 0 && !ctx.TryWriteUInt64(resultAddress, existedSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (existedSize < memorySize)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
using var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
stream.SetLength((long)memorySize);
|
||||
}
|
||||
|
||||
TraceSaveData($"memory-setup2 user={userId} size=0x{memorySize:X} existed=0x{existedSize:X}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QwOO7vegnV8",
|
||||
ExportName = "sceSaveDataGetSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataGetSaveDataMemory2(CpuContext ctx) =>
|
||||
TransferSaveDataMemory(ctx, write: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "cduy9v4YmT4",
|
||||
ExportName = "sceSaveDataSetSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSetSaveDataMemory2(CpuContext ctx) =>
|
||||
TransferSaveDataMemory(ctx, write: true);
|
||||
|
||||
// Writes go straight through to the backing file, so a ready state is
|
||||
// all sync has to confirm.
|
||||
[SysAbiExport(
|
||||
Nid = "wiT9jeC7xPw",
|
||||
ExportName = "sceSaveDataSyncSaveDataMemory",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSyncSaveDataMemory(CpuContext ctx)
|
||||
{
|
||||
var syncAddress = ctx[CpuRegister.Rdi];
|
||||
if (syncAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, syncAddress, out var userId))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(
|
||||
File.Exists(ResolveSaveDataMemoryPath(userId)) ? 0 : OrbisSaveDataErrorMemoryNotReady);
|
||||
}
|
||||
|
||||
private static int TransferSaveDataMemory(CpuContext ctx, bool write)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, requestAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(requestAddress + 0x08, out var dataAddress))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = ResolveSaveDataMemoryPath(userId);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorMemoryNotReady);
|
||||
}
|
||||
|
||||
if (dataAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (!TryReadMemoryData(ctx, dataAddress, out var bufAddress, out var bufSize, out var offset))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
using var stream = new FileStream(
|
||||
path, FileMode.Open, write ? FileAccess.ReadWrite : FileAccess.Read);
|
||||
var length = (ulong)stream.Length;
|
||||
if (bufAddress == 0 || bufSize > length || offset > length - bufSize)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
// The guarded file length bounds bufSize, so one rented buffer
|
||||
// covers the transfer and a guest fault never partially writes.
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Max(bufSize, 1));
|
||||
try
|
||||
{
|
||||
var span = buffer.AsSpan(0, (int)bufSize);
|
||||
stream.Seek((long)offset, SeekOrigin.Begin);
|
||||
if (write)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(bufAddress, span))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
stream.Write(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.ReadExactly(span);
|
||||
if (!ctx.Memory.TryWrite(bufAddress, span))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"memory-{(write ? "set2" : "get2")} user={userId} offset=0x{offset:X} size=0x{bufSize:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.SaveData;
|
||||
|
||||
[CollectionDefinition("SaveDataMemoryState", DisableParallelization = true)]
|
||||
public sealed class SaveDataMemoryStateCollection;
|
||||
|
||||
// The save data memory exports persist to a real backing file whose resolved
|
||||
// path depends on process-wide environment and title configuration. The
|
||||
// fixture pins both and the collection keeps other environment-mutating tests
|
||||
// from running alongside.
|
||||
[Collection("SaveDataMemoryState")]
|
||||
public sealed class SaveDataMemoryExportsTests : IDisposable
|
||||
{
|
||||
private const ulong Base = 0x1_0000_0000;
|
||||
private const ulong SetupParamAddress = Base + 0x100;
|
||||
private const ulong SetupResultAddress = Base + 0x180;
|
||||
private const ulong DataStructAddress = Base + 0x200;
|
||||
private const ulong RequestAddress = Base + 0x280;
|
||||
private const ulong SyncParamAddress = Base + 0x300;
|
||||
private const ulong PayloadAddress = Base + 0x400;
|
||||
private const ulong ReadbackAddress = Base + 0x800;
|
||||
private const ulong LargePayloadAddress = Base + 0x1000;
|
||||
private const ulong LargeReadbackAddress = Base + 0x40000;
|
||||
private const ulong MemorySize = 0x2000;
|
||||
private const ulong UnmappedAddress = Base + 0x100000;
|
||||
private const int MemoryNotReady = unchecked((int)0x809F0012);
|
||||
private const int ParameterError = unchecked((int)0x809F0000);
|
||||
private const int UserId = 0x1001;
|
||||
private const string TitleId = "SDMEMTEST";
|
||||
|
||||
private readonly FakeCpuMemory _memory = new(Base, 0x80000);
|
||||
private readonly CpuContext _ctx;
|
||||
private readonly string _root;
|
||||
private readonly string? _previousRoot;
|
||||
|
||||
public SaveDataMemoryExportsTests()
|
||||
{
|
||||
_ctx = new CpuContext(_memory, Generation.Gen5);
|
||||
_root = Path.Combine(Path.GetTempPath(), $"sharpemu-sdmemory-{Guid.NewGuid():N}");
|
||||
_previousRoot = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR", _root);
|
||||
SaveDataExports.ConfigureApplicationInfo(TitleId);
|
||||
}
|
||||
|
||||
private string MemoryPath =>
|
||||
Path.Combine(_root, UserId.ToString(), TitleId, "sce_sdmemory", "memory.dat");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SaveDataExports.ConfigureApplicationInfo(null);
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR", _previousRoot);
|
||||
if (Directory.Exists(_root))
|
||||
{
|
||||
Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setup_ReportsExistingSizeOnSecondCall()
|
||||
{
|
||||
Assert.Equal(0, Setup());
|
||||
Assert.True(_ctx.TryReadUInt64(SetupResultAddress, out var existedSize));
|
||||
Assert.Equal(0ul, existedSize);
|
||||
|
||||
Assert.Equal(0, Setup());
|
||||
Assert.True(_ctx.TryReadUInt64(SetupResultAddress, out existedSize));
|
||||
Assert.Equal(MemorySize, existedSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetThenGet_RoundTripsThroughBackingFile()
|
||||
{
|
||||
Assert.Equal(0, Setup());
|
||||
|
||||
var payload = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
|
||||
Assert.True(_ctx.Memory.TryWrite(PayloadAddress, payload));
|
||||
WriteRequest(PayloadAddress, (ulong)payload.Length, offset: 0x40);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
||||
|
||||
WriteRequest(ReadbackAddress, (ulong)payload.Length, offset: 0x40);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
var readback = new byte[payload.Length];
|
||||
Assert.True(_ctx.Memory.TryRead(ReadbackAddress, readback));
|
||||
Assert.Equal(payload, readback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetThenGet_LargePayload_RoundTrips()
|
||||
{
|
||||
const ulong size = 0x30000;
|
||||
Assert.Equal(0, Setup(0x40000));
|
||||
|
||||
var payload = new byte[size];
|
||||
for (var i = 0; i < payload.Length; i++)
|
||||
{
|
||||
payload[i] = (byte)(i * 31 + 7);
|
||||
}
|
||||
|
||||
Assert.True(_ctx.Memory.TryWrite(LargePayloadAddress, payload));
|
||||
WriteRequest(LargePayloadAddress, size, offset: 0x100);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
||||
|
||||
WriteRequest(LargeReadbackAddress, size, offset: 0x100);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
var readback = new byte[size];
|
||||
Assert.True(_ctx.Memory.TryRead(LargeReadbackAddress, readback));
|
||||
Assert.Equal(payload, readback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setup_AbsurdSize_ReturnsParameterError()
|
||||
{
|
||||
Assert.Equal(ParameterError, Setup(ulong.MaxValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setup_InvalidResultPointer_DoesNotCreateBackingFile()
|
||||
{
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
Setup(resultAddress: UnmappedAddress));
|
||||
Assert.False(File.Exists(MemoryPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setup_InvalidResultPointer_DoesNotGrowExistingFile()
|
||||
{
|
||||
Assert.Equal(0, Setup(0x1000));
|
||||
Assert.Equal(
|
||||
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
|
||||
Setup(MemorySize, UnmappedAddress));
|
||||
Assert.Equal(0x1000, new FileInfo(MemoryPath).Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setup_GrowingExistingMemory_ZeroExtendsAndPreservesContent()
|
||||
{
|
||||
Assert.Equal(0, Setup(0x1000));
|
||||
var payload = new byte[] { 0x11, 0x22 };
|
||||
Assert.True(_ctx.Memory.TryWrite(PayloadAddress, payload));
|
||||
WriteRequest(PayloadAddress, (ulong)payload.Length, offset: 0xFF0);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
||||
|
||||
Assert.Equal(0, Setup(MemorySize));
|
||||
Assert.True(_ctx.TryReadUInt64(SetupResultAddress, out var existedSize));
|
||||
Assert.Equal(0x1000ul, existedSize);
|
||||
|
||||
WriteRequest(ReadbackAddress, 0x20, offset: 0xFF0);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
var readback = new byte[0x20];
|
||||
Assert.True(_ctx.Memory.TryRead(ReadbackAddress, readback));
|
||||
Assert.Equal(payload, readback[..2]);
|
||||
Assert.All(readback[2..], b => Assert.Equal(0, b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSetSync_BeforeSetup_ReturnMemoryNotReady()
|
||||
{
|
||||
WriteRequest(ReadbackAddress, 0x10, offset: 0);
|
||||
Assert.Equal(MemoryNotReady, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
Assert.Equal(MemoryNotReady, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
||||
Assert.Equal(MemoryNotReady, Sync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAndSync_AfterSetup_Succeed()
|
||||
{
|
||||
Assert.Equal(0, Setup());
|
||||
WriteRequest(ReadbackAddress, 0x10, offset: 0);
|
||||
Assert.Equal(0, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
Assert.Equal(0, Sync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Get_BackingFileRemovedAfterSetup_ReturnsMemoryNotReady()
|
||||
{
|
||||
Assert.Equal(0, Setup());
|
||||
Directory.Delete(_root, recursive: true);
|
||||
WriteRequest(ReadbackAddress, 0x10, offset: 0);
|
||||
Assert.Equal(MemoryNotReady, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MemorySize, 1ul)]
|
||||
[InlineData(0ul, MemorySize + 1)]
|
||||
[InlineData(ulong.MaxValue, 0x10ul)]
|
||||
public void Get_OutOfRange_ReturnsParameterError(ulong offset, ulong size)
|
||||
{
|
||||
Assert.Equal(0, Setup());
|
||||
WriteRequest(ReadbackAddress, size, offset);
|
||||
Assert.Equal(ParameterError, SaveDataExports.SaveDataGetSaveDataMemory2(Invoke()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MemorySize, 1ul)]
|
||||
[InlineData(0ul, MemorySize + 1)]
|
||||
[InlineData(ulong.MaxValue, 0x10ul)]
|
||||
public void Set_OutOfRange_ReturnsParameterError(ulong offset, ulong size)
|
||||
{
|
||||
Assert.Equal(0, Setup());
|
||||
WriteRequest(PayloadAddress, size, offset);
|
||||
Assert.Equal(ParameterError, SaveDataExports.SaveDataSetSaveDataMemory2(Invoke()));
|
||||
}
|
||||
|
||||
private int Setup(ulong memorySize = MemorySize, ulong resultAddress = SetupResultAddress)
|
||||
{
|
||||
Span<byte> param = stackalloc byte[0x40];
|
||||
param.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(param[0x04..], UserId);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(param[0x08..], memorySize);
|
||||
Assert.True(_ctx.Memory.TryWrite(SetupParamAddress, param));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = SetupParamAddress;
|
||||
_ctx[CpuRegister.Rsi] = resultAddress;
|
||||
return SaveDataExports.SaveDataSetupSaveDataMemory2(_ctx);
|
||||
}
|
||||
|
||||
private int Sync()
|
||||
{
|
||||
Span<byte> param = stackalloc byte[0x28];
|
||||
param.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(param, UserId);
|
||||
Assert.True(_ctx.Memory.TryWrite(SyncParamAddress, param));
|
||||
|
||||
_ctx[CpuRegister.Rdi] = SyncParamAddress;
|
||||
return SaveDataExports.SaveDataSyncSaveDataMemory(_ctx);
|
||||
}
|
||||
|
||||
private void WriteRequest(ulong bufAddress, ulong bufSize, ulong offset)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[0x18];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data, bufAddress);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0x08..], bufSize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0x10..], offset);
|
||||
Assert.True(_ctx.Memory.TryWrite(DataStructAddress, data));
|
||||
|
||||
Span<byte> request = stackalloc byte[0x10];
|
||||
request.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(request, UserId);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(request[0x08..], DataStructAddress);
|
||||
Assert.True(_ctx.Memory.TryWrite(RequestAddress, request));
|
||||
}
|
||||
|
||||
private CpuContext Invoke()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = RequestAddress;
|
||||
return _ctx;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user