mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
[Json] Implement sce::Json::Value and String (construct / set / destroy) (#169)
* [Json] Implement sce::Json::Value and Json::String construct/set/destroy libSceJson previously only had the Initializer/MemAllocator setup path. The Value and String classes themselves were entirely absent, so a Prospero title that builds a JSON tree (Quake PPSA01880 does, to shape a web-API request) hit unresolved imports and faulted on the call. The imports it left unresolved right before its access violation are exactly these Value ctors/setters and String ctor/dtor. Model the Value/String payload host-side (JsonObjectHeap), keyed by the guest `this` pointer, following the handle-shadow pattern already used by Ngs2Exports. The guest object bytes are deliberately not written: these objects are usually stack-allocated with an unknown real layout, and writing a guessed layout risks smashing an adjacent stack canary (the same hazard the AudioOut2 context-param note in this tree records). Constructors and setters follow the Itanium ABI and return `this` in rax, which is correct whether the real setter returns void or Value&. Covered NIDs (complete-object C1/D1 variants, matching the observed imports): Value(default/bool/long/ulong/double/ValueType/char*/String), Value::~Value, Value::set(bool/long/ulong/double/ValueType/char*/String), Value::clear, String(char*/default/copy), String::~String. Only the payload the guest can reach through library methods is modelled; direct guest reads of the object bytes are out of scope and would need observed layout evidence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [Tests] Add SharpEmu.Libs.Tests covering the Json Value/String exports First test project for SharpEmu.Libs (xunit), the SharpEmu.Libs.Tests layout the maintainer already agreed to in issue #36. - A FakeCpuMemory (single contiguous region) drives the exports at the CpuContext level with no live guest. - Direct-call tests: ctor/setter round-trips for bool/int/uint/double (read from xmm0)/char*/String/ValueType, destructor cleanup, and the graceful-degradation paths (missing String shadow and a faulting char* pointer both fall back to the empty string instead of throwing). - Registration test: a real ModuleManager scans SharpEmu.Libs and the nine NIDs Quake left unresolved now resolve to the libSceJson exports and dispatch cleanly (returns `this` in rax). InternalsVisibleTo exposes JsonObjectHeap to the test assembly. The test project's packages.lock.json is committed for CI locked-mode restore; CI does not run tests yet, left as a maintainer decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [Json] Add Initializer::setGlobalNullAccessCallback Quake calls it during kexPSNWebAPI::Initialize and treats the not-found error as fatal for the whole Np Web API bring-up. Store the guest hook (never invoked by this HLE: shadows degrade to defaults instead of dereferencing missing members) and return success. Verified against the dump: the "setGlobalNullAccessCallback failed (0x80020002)" line is gone and kexPSNWebAPI::Initialize now logs "Np Web API Initialized"; the next blockers are sceNpAuthCreateRequest and sceUserServiceInitialize ordering, outside libSceJson. Also pins both Json test classes to one xunit collection: they share JsonObjectHeap statics and parallel class execution raced ResetForTests against a running test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
4c35831cb8
commit
df53ff59d9
@@ -78,6 +78,30 @@ public static class JsonExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
|
||||
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
|
||||
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
|
||||
[SysAbiExport(
|
||||
Nid = "+drDFyAS6u4",
|
||||
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceJson")]
|
||||
public static int InitializerSetGlobalNullAccessCallback(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
if (thisAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
|
||||
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
|
||||
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WSOuge5IsCg",
|
||||
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Json;
|
||||
|
||||
// sce::Json::Value and sce::Json::String constructors, setters and destructors. Prospero titles
|
||||
// (Quake among them) build a Value tree and populate it through these before serializing it for a
|
||||
// web request; without them the imports resolve to nothing and the guest faults on the call. The
|
||||
// payload is modelled in JsonObjectHeap; here we only translate the C++ ABI (registers in, `this`
|
||||
// back out) and never write the guest object, so a stack-allocated Value/String is left intact.
|
||||
//
|
||||
// Only the complete-object variants (C1/D1) are bound — those are what standalone locals emit and
|
||||
// what the observed Prospero imports use. The base-object variants (C2/D2) are left for a title
|
||||
// that actually imports them.
|
||||
public static class JsonValueExports
|
||||
{
|
||||
private const int MaxStringLength = 0x10000;
|
||||
|
||||
private static int ReturnThis(CpuContext ctx, ulong thisAddress)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = thisAddress;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int ReturnVoid(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static double ReadDoubleArg(CpuContext ctx)
|
||||
{
|
||||
ctx.GetXmmRegister(0, out var low, out _);
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)low));
|
||||
}
|
||||
|
||||
private static string ReadCString(CpuContext ctx, ulong address) =>
|
||||
ctx.TryReadNullTerminatedUtf8(address, MaxStringLength, out var text) ? text : string.Empty;
|
||||
|
||||
// ---- sce::Json::Value constructors ----
|
||||
|
||||
[SysAbiExport(Nid = "qBMjqyBn3OM", ExportName = "_ZN3sce4Json5ValueC1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueDefaultConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "UeuWT+yNdCQ", ExportName = "_ZN3sce4Json5ValueC1Eb",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueBooleanConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "0lLK8+kDqmE", ExportName = "_ZN3sce4Json5ValueC1El",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueIntegerConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "x4AUdbhpRB0", ExportName = "_ZN3sce4Json5ValueC1Em",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueUnsignedConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "sOmU4vnx3s0", ExportName = "_ZN3sce4Json5ValueC1Ed",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueRealConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "b9V6fmppLXY", ExportName = "_ZN3sce4Json5ValueC1EPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueCStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "CbrT3dwDILo", ExportName = "_ZN3sce4Json5ValueC1ENS0_9ValueTypeE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueTypeConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "sZIoMRGO+jk", ExportName = "_ZN3sce4Json5ValueC1ERKNS0_6StringE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "WTtYf+cNnXI", ExportName = "_ZN3sce4Json5ValueD1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueDestructor(CpuContext ctx)
|
||||
{
|
||||
JsonObjectHeap.RemoveValue(ctx[CpuRegister.Rdi]);
|
||||
return ReturnVoid(ctx);
|
||||
}
|
||||
|
||||
// ---- sce::Json::Value setters (return Value&, i.e. `this`) ----
|
||||
|
||||
[SysAbiExport(Nid = "5yHuiWXo2gg", ExportName = "_ZN3sce4Json5Value3setEb",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetBoolean(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "QxVVYhP-mvg", ExportName = "_ZN3sce4Json5Value3setEl",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetInteger(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "SIe1ZmW7e7s", ExportName = "_ZN3sce4Json5Value3setEm",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetUnsigned(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "BSmWDIkV4w4", ExportName = "_ZN3sce4Json5Value3setEd",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetReal(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "IKQimvG9Wqs", ExportName = "_ZN3sce4Json5Value3setENS0_9ValueTypeE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetType(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "n6FC+l9DU70", ExportName = "_ZN3sce4Json5Value3setEPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetCString(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "6l3Bv2gysNc", ExportName = "_ZN3sce4Json5Value3setERKNS0_6StringE",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueSetString(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "FIjXN2TkuTs", ExportName = "_ZN3sce4Json5Value5clearEv",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int ValueClear(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
// ---- sce::Json::String ----
|
||||
|
||||
[SysAbiExport(Nid = "9KUZFjI1IxA", ExportName = "_ZN3sce4Json6StringC1EPKc",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringCStringConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, ReadCString(ctx, ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "qSmqLXXCPas", ExportName = "_ZN3sce4Json6StringC1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringDefaultConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, string.Empty);
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "0CAesfH963Q", ExportName = "_ZN3sce4Json6StringC1ERKS1_",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringCopyConstructor(CpuContext ctx)
|
||||
{
|
||||
var thisAddress = ctx[CpuRegister.Rdi];
|
||||
JsonObjectHeap.SetString(thisAddress, JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi]));
|
||||
return ReturnThis(ctx, thisAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(Nid = "cG1VE2HMl6c", ExportName = "_ZN3sce4Json6StringD1Ev",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
|
||||
public static int StringDestructor(CpuContext ctx)
|
||||
{
|
||||
JsonObjectHeap.RemoveString(ctx[CpuRegister.Rdi]);
|
||||
return ReturnVoid(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SharpEmu.Libs.Json;
|
||||
|
||||
// sce::Json::Value is an opaque variant type (null / bool / signed / unsigned / real / string /
|
||||
// array / object). Games build one, populate it through set()/ctors and later serialize it. We
|
||||
// model the payload host-side keyed by the guest `this` pointer instead of writing into the guest
|
||||
// object: the object is often stack-allocated and its real byte layout is unknown, so writing a
|
||||
// guessed layout risks smashing an adjacent stack canary (the same failure the AudioOut2 context
|
||||
// param sizing note in this project already ran into). The guest reaches the payload only through
|
||||
// libSceJson methods, so shadowing by address is enough for the build path.
|
||||
internal enum JsonValueKind : byte
|
||||
{
|
||||
Null = 0,
|
||||
Boolean = 1,
|
||||
Integer = 2,
|
||||
UInteger = 3,
|
||||
Real = 4,
|
||||
String = 5,
|
||||
|
||||
// set(ValueType) / Value(ValueType): the guest chose the type itself. We keep its raw enum
|
||||
// value verbatim rather than mapping it, because the canonical ValueType constants are not
|
||||
// known from clean-room evidence and round-tripping the guest's own value is what matters.
|
||||
ExplicitType = 6,
|
||||
}
|
||||
|
||||
internal readonly struct JsonValueState
|
||||
{
|
||||
private JsonValueState(
|
||||
JsonValueKind kind,
|
||||
bool boolean = false,
|
||||
long integer = 0,
|
||||
ulong unsignedInteger = 0,
|
||||
double real = 0,
|
||||
string? text = null,
|
||||
uint explicitType = 0)
|
||||
{
|
||||
Kind = kind;
|
||||
Boolean = boolean;
|
||||
Integer = integer;
|
||||
UnsignedInteger = unsignedInteger;
|
||||
Real = real;
|
||||
Text = text;
|
||||
ExplicitType = explicitType;
|
||||
}
|
||||
|
||||
public JsonValueKind Kind { get; }
|
||||
public bool Boolean { get; }
|
||||
public long Integer { get; }
|
||||
public ulong UnsignedInteger { get; }
|
||||
public double Real { get; }
|
||||
public string? Text { get; }
|
||||
public uint ExplicitType { get; }
|
||||
|
||||
public static JsonValueState Null { get; } = new(JsonValueKind.Null);
|
||||
|
||||
public static JsonValueState FromBoolean(bool value) => new(JsonValueKind.Boolean, boolean: value);
|
||||
|
||||
public static JsonValueState FromInteger(long value) => new(JsonValueKind.Integer, integer: value);
|
||||
|
||||
public static JsonValueState FromUnsignedInteger(ulong value) =>
|
||||
new(JsonValueKind.UInteger, unsignedInteger: value);
|
||||
|
||||
public static JsonValueState FromReal(double value) => new(JsonValueKind.Real, real: value);
|
||||
|
||||
public static JsonValueState FromString(string value) => new(JsonValueKind.String, text: value);
|
||||
|
||||
public static JsonValueState FromExplicitType(uint value) =>
|
||||
new(JsonValueKind.ExplicitType, explicitType: value);
|
||||
}
|
||||
|
||||
// Shared host-side heap for the libSceJson object shadows. Keyed by the guest object address;
|
||||
// constructors overwrite and destructors remove, so guest stack-address reuse stays correct.
|
||||
internal static class JsonObjectHeap
|
||||
{
|
||||
public static ConcurrentDictionary<ulong, JsonValueState> Values { get; } = new();
|
||||
|
||||
public static ConcurrentDictionary<ulong, string> Strings { get; } = new();
|
||||
|
||||
// Guest function the library should call when a Value is read as the wrong type. This HLE
|
||||
// never dereferences missing members (shadows degrade to defaults), so the hook is stored for
|
||||
// fidelity but not invoked.
|
||||
public static ulong GlobalNullAccessCallback;
|
||||
|
||||
public static ulong GlobalNullAccessCallbackContext;
|
||||
|
||||
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
|
||||
|
||||
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
|
||||
|
||||
public static void SetString(ulong address, string text) => Strings[address] = text;
|
||||
|
||||
public static void RemoveString(ulong address) => Strings.TryRemove(address, out _);
|
||||
|
||||
// A missing shadow (temporary the compiler built without an out-of-line ctor, or a copy we did
|
||||
// not track) degrades to the empty string rather than faulting.
|
||||
public static string GetStringOrEmpty(ulong address) =>
|
||||
Strings.TryGetValue(address, out var text) ? text : string.Empty;
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
Values.Clear();
|
||||
Strings.Clear();
|
||||
GlobalNullAccessCallback = 0;
|
||||
GlobalNullAccessCallbackContext = 0;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
|
||||
Reference in New Issue
Block a user