Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ under [1.0.0](#100---2026-09-20) before upgrading.

## [Unreleased]

- `AddressResolutionOptions.UseHostSymbolTable` is retained only for source and binary compatibility and is rejected by
`EngineInspection.ResolveAddress`; use `EngineInspection.ResolveHostAddress` for host-symbol resolution.
- Removed the obsolete host-symbol compatibility flag from `AddressResolutionOptions`; use
`EngineInspection.ResolveHostAddress` for host-symbol resolution.

## [1.0.0] - 2026-09-20

Expand Down

Large diffs are not rendered by default.

182 changes: 97 additions & 85 deletions libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ namespace CheatEngine.SDK.Abi.Native;
/// <c>ContinueDebugEvent</c> cell is not yet a qualified SDK ABI surface.
/// </para>
/// </remarks>
internal sealed unsafe class ClassicDebugEventDispatcher : IDisposable
internal sealed unsafe class
ClassicDebugEventDispatcher : IDisposable // NOSONAR: this type is the native debug-event ABI boundary.
{
private static readonly Lock SRegistrationGate = new();
private static ClassicDebugEventDispatcher? s_active;
Expand Down Expand Up @@ -152,64 +153,29 @@ internal static ClassicDebugEventRegistrationStatus TryRegister(
/// </summary>
internal ClassicDebugEventReleaseStatus TryRelease()
{
if (ReferenceEquals(t_dispatcher, this))
if (!TryBeginRelease(out ClassicDebugEventReleaseStatus releaseStatus))
{
return ClassicDebugEventReleaseStatus.CallbackIsExecuting;
return releaseStatus;
}

lock (_gate)
{
if (_released)
{
return ClassicDebugEventReleaseStatus.Released;
}

if (_releaseInProgress)
{
return ClassicDebugEventReleaseStatus.ReleaseInProgress;
}

_acceptingCallbacks = false;
_releaseInProgress = true;
}

if (!TryUnregister())
{
return ClassicDebugEventReleaseStatus.UnregisterUnconfirmed;
}

CompleteRelease();
return ClassicDebugEventReleaseStatus.Released;
}

private bool TryUnregister()
{
try
{
if (_unregisterFunction(_pluginId, _functionId).IsTrue)
if (!_unregisterFunction(_pluginId, _functionId).IsTrue)
{
return true;
EndUnconfirmedReleaseAttempt();
return ClassicDebugEventReleaseStatus.UnregisterUnconfirmed;
}
}
catch (Exception)
{
// The host can fail without throwing or can throw from the unmanaged call.
EndUnconfirmedReleaseAttempt();
return ClassicDebugEventReleaseStatus.UnregisterUnconfirmed;
}

EndUnconfirmedReleaseAttempt();
return false;
}

private void CompleteRelease()
{
_callbacksDrained.Wait();
lock (SRegistrationGate)
{
if (ReferenceEquals(s_active, this))
{
s_active = null;
}
}
// Unregistration has already closed admission. The drain is therefore deliberately uninterruptible: allowing
// cancellation here would release the managed callback root while a native callback could still be in flight.
_callbacksDrained.Wait(CancellationToken.None);
ClearActiveRegistration(this);

DebugEventPluginInit* initialization;
lock (_gate)
Expand All @@ -228,6 +194,7 @@ private void CompleteRelease()
}

_callbacksDrained.Dispose();
return ClassicDebugEventReleaseStatus.Released;
}

private static int Dispatch(void* nativeEvent)
Expand All @@ -239,13 +206,9 @@ private static int Dispatch(void* nativeEvent)
return 0;
}

ClassicDebugEventDispatcher? dispatcher;
lock (SRegistrationGate)
{
dispatcher = s_active;
}

if (dispatcher is null || !dispatcher.TryEnterCallback(out DebugEventDecisionHandler handler,
if (!TryEnterActiveCallback(
out ClassicDebugEventDispatcher? dispatcher,
out DebugEventDecisionHandler handler,
out BoundedDebugEventObservationBuffer? observations))
{
return 0;
Expand All @@ -255,7 +218,33 @@ private static int Dispatch(void* nativeEvent)
t_dispatcher = dispatcher;
try
{
return InvokeHandler(dispatcher, handler, observations, nativeEvent);
DebugEventHeader header = Unsafe.ReadUnaligned<DebugEventHeader>(nativeEvent);
DebugEventObservation observation = new(
Interlocked.Increment(ref s_nextSequenceNumber),
header.EventCode,
header.ProcessId,
header.ThreadId);

// This is a bounded copy, never a continuation path. It cannot call a consumer or await work.
observations?.TryPublish(in observation);

DebugEventDecision decision;
try
{
decision = handler(in observation);
}
catch (Exception)
{
Interlocked.Increment(ref dispatcher._callbackFailureCount);
return 0;
}

if (decision is not DebugEventDecision.ContinueWithCheatEngine)
{
Interlocked.Increment(ref dispatcher._unsupportedContinuationRequestCount);
}

return 0;
}
catch (Exception)
{
Expand All @@ -270,39 +259,57 @@ private static int Dispatch(void* nativeEvent)
}
}

private static int InvokeHandler(
ClassicDebugEventDispatcher dispatcher,
DebugEventDecisionHandler handler,
BoundedDebugEventObservationBuffer? observations,
void* nativeEvent)
private bool TryBeginRelease(out ClassicDebugEventReleaseStatus releaseStatus)
{
DebugEventHeader header = Unsafe.ReadUnaligned<DebugEventHeader>(nativeEvent);
DebugEventObservation observation = new(
Interlocked.Increment(ref s_nextSequenceNumber),
header.EventCode,
header.ProcessId,
header.ThreadId);

// This is a bounded copy, never a continuation path. It cannot call a consumer or await work.
observations?.TryPublish(in observation);
if (ReferenceEquals(t_dispatcher, this))
{
releaseStatus = ClassicDebugEventReleaseStatus.CallbackIsExecuting;
return false;
}

DebugEventDecision decision;
try
lock (_gate)
{
decision = handler(in observation);
if (_released)
{
releaseStatus = ClassicDebugEventReleaseStatus.Released;
return false;
}

if (_releaseInProgress)
{
releaseStatus = ClassicDebugEventReleaseStatus.ReleaseInProgress;
return false;
}

_acceptingCallbacks = false;
_releaseInProgress = true;
}
catch (Exception)

releaseStatus = default;
return true;
}

private static bool TryEnterActiveCallback(
out ClassicDebugEventDispatcher dispatcher,
out DebugEventDecisionHandler handler,
out BoundedDebugEventObservationBuffer? observations)
{
ClassicDebugEventDispatcher? active;
lock (SRegistrationGate)
{
Interlocked.Increment(ref dispatcher._callbackFailureCount);
return 0;
active = s_active;
}

if (decision is not DebugEventDecision.ContinueWithCheatEngine)
if (active is null)
{
Interlocked.Increment(ref dispatcher._unsupportedContinuationRequestCount);
dispatcher = null!;
handler = null!;
observations = null;
return false;
}

return 0;
dispatcher = active;
return dispatcher.TryEnterCallback(out handler, out observations);
}

private bool TryEnterCallback(
Expand Down Expand Up @@ -366,13 +373,7 @@ private void AbandonFailedRegistration()
_releaseInProgress = false;
}

lock (SRegistrationGate)
{
if (ReferenceEquals(s_active, this))
{
s_active = null;
}
}
ClearActiveRegistration(this);

if (initialization is not null)
{
Expand All @@ -382,6 +383,17 @@ private void AbandonFailedRegistration()
_callbacksDrained.Dispose();
}

private static void ClearActiveRegistration(ClassicDebugEventDispatcher dispatcher)
{
lock (SRegistrationGate)
{
if (ReferenceEquals(s_active, dispatcher))
{
s_active = null;
}
}
}

private void EndUnconfirmedReleaseAttempt()
{
lock (_gate)
Expand Down
57 changes: 34 additions & 23 deletions libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,34 +120,13 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem
bool mutationStarted = false;
try
{
MemoryRecordMutationOutcome preflight = TryGetCurrentList(state, out AddressList list);
MemoryRecordMutationOutcome preflight = TryPrepareParentAssignment(state, recordId, parentId,
traversalLimit, out MemoryRecord child, out MemoryRecord parent);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

preflight = TryResolveRecord(state, list, recordId, false, out MemoryRecord child);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

MemoryRecord parent = default;
if (parentId.HasValue)
{
preflight = TryResolveRecord(state, list, parentId.Value, true, out parent);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

preflight = ValidateParentChain(state, recordId, parent, traversalLimit);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}
}

if (LuaRuntime.CurrentStateIdentity != identity)
{
return NotAttempted(MemoryRecordMutationProblem.GlobalUnavailable);
Expand Down Expand Up @@ -179,6 +158,38 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem
}
}

private static MemoryRecordMutationOutcome TryPrepareParentAssignment(LuaState state, MemoryRecordId recordId,
MemoryRecordId? parentId, MemoryRecordParentTraversalLimit traversalLimit, out MemoryRecord child,
out MemoryRecord parent)
{
child = default;
parent = default;
MemoryRecordMutationOutcome preflight = TryGetCurrentList(state, out AddressList list);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

preflight = TryResolveRecord(state, list, recordId, false, out child);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

if (!parentId.HasValue)
{
return Completed();
}

preflight = TryResolveRecord(state, list, parentId.Value, true, out parent);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

return ValidateParentChain(state, recordId, parent, traversalLimit);
}

private static MemoryRecordMutationOutcome TryResolveRecord(LuaState state, AddressList list, MemoryRecordId id,
bool isParent, out MemoryRecord record)
{
Expand Down
Loading
Loading