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
178 changes: 133 additions & 45 deletions libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,17 @@

/// <summary>Assigns a record's parent after validating the requested hierarchy with the default traversal bound.</summary>
/// <param name="recordId">The child record's CE ID in the current list.</param>
/// <param name="parentId">The new parent's CE ID, or <see langword="null" /> to make the child a root record.</param>
/// <param name="parentId">
/// The new parent's CE ID, or <see langword="null" /> to require that the child is already a root record.
/// </param>
/// <returns>A command result that preserves whether assignment was started.</returns>
/// <exception cref="InvalidOperationException">The plugin is not enabled or the calling thread has no Lua state.</exception>
/// <remarks>
/// Cheat Engine exposes no safe Lua operation for detaching an existing nested record to the address-list root.
/// A <see langword="null" /> request therefore completes only when the protected <c>Parent</c> read already
/// reports a root record; otherwise it is refused with
/// <see cref="MemoryRecordMutationProblem.GlobalUnavailable" /> before the setter is called.
/// </remarks>
[RequiresPluginEnabled]
public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, MemoryRecordId? parentId)
{
Expand All @@ -111,7 +119,9 @@

/// <summary>Assigns a record's parent after validating the requested hierarchy with <paramref name="traversalLimit" />.</summary>
/// <param name="recordId">The child record's CE ID in the current list.</param>
/// <param name="parentId">The new parent's CE ID, or <see langword="null" /> to make the child a root record.</param>
/// <param name="parentId">
/// The new parent's CE ID, or <see langword="null" /> to require that the child is already a root record.
/// </param>
/// <param name="traversalLimit">A positive bound for the proposed parent's existing parent chain.</param>
/// <returns>A command result that preserves whether assignment was started.</returns>
/// <exception cref="InvalidOperationException">The plugin is not enabled or the calling thread has no Lua state.</exception>
Expand Down Expand Up @@ -139,30 +149,86 @@
bool mutationStarted = false;
try
{
MemoryRecordMutationOutcome preflight = TryPrepareParentAssignment(state, recordId, parentId,
traversalLimit, out MemoryRecord child, out MemoryRecord parent);
MemoryRecordMutationOutcome preflight = TryGetCurrentList(state, out AddressList list);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

if (LuaRuntime.CurrentStateIdentity != identity)
preflight = TryResolveRecord(state, list, recordId, isParent: false, out MemoryRecord child);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged);
return preflight;
}

if (!parentId.HasValue)
{
preflight = TryReadParentId(state, child, out bool hasParent, out _);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return preflight;
}

if (LuaRuntime.CurrentStateIdentity != identity)

Check warning on line 172 in libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'if' statement can be simplified

See more on https://sonarcloud.io/project/issues?id=CheatEngineNet_CheatEngine.SDK&issues=AaFUBEY4woTQ6jdsKTKY&open=AaFUBEY4woTQ6jdsKTKY&pullRequest=96
{
return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged);
}

return hasParent
? NotAttempted(MemoryRecordMutationProblem.GlobalUnavailable)
: Completed();
}

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

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

if (LuaRuntime.CurrentStateIdentity != identity)
{
return NotAttempted(MemoryRecordMutationProblem.RuntimeIdentityChanged);
}

parent.Handle.Push(state);
mutationStarted = true;
LuaStatus status = child.Handle.TrySetProperty(state, "Parent"u8);
return status.IsOk ? Completed() : Indeterminate(status);
if (!status.IsOk)
{
return Indeterminate(status);
}

// Admission prevents supported state transitions. Re-resolve records after callbacks and verify identity before success.
preflight = TryGetCurrentList(state, out list);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return IndeterminateAfterParentSetter(preflight);
}

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

preflight = TryReadParentId(state, rereadChild, out bool hasActualParent, out MemoryRecordId actualParent);
if (preflight.Problem != MemoryRecordMutationProblem.None)
{
return IndeterminateAfterParentSetter(preflight);
}

if (LuaRuntime.CurrentStateIdentity != identity || !hasActualParent || actualParent != parentId.Value)

Check warning on line 226 in libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'if' statement can be simplified

See more on https://sonarcloud.io/project/issues?id=CheatEngineNet_CheatEngine.SDK&issues=AaFUBEY4woTQ6jdsKTKZ&open=AaFUBEY4woTQ6jdsKTKZ&pullRequest=96
{
return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok);
}

return Completed();
}
catch (LuaException exception)
{
Expand Down Expand Up @@ -335,38 +401,6 @@
return new MemoryRecordActivationOutcome(kind, problem, requested, before, after, asyncProcessing, status);
}

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 Expand Up @@ -479,6 +513,49 @@
}
}

private static MemoryRecordMutationOutcome TryReadParentId(LuaState state, MemoryRecord child, out bool hasParent,
out MemoryRecordId parentId)
{
int top = state.Top;
hasParent = false;
parentId = default;
LuaStatus status = child.Handle.TryGetProperty(state, "Parent"u8);
if (!status.IsOk)
{
state.SetTop(top);
return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status);
}

if (state.IsNil(-1))
{
state.SetTop(top);
return Completed();
}

if (!MemoryRecord.TryRead(state, -1, out MemoryRecord parent))
{
state.SetTop(top);
return NotAttempted(MemoryRecordMutationProblem.InvalidResult);
}

status = parent.Handle.TryGetProperty(state, "ID"u8);
if (!status.IsOk)
{
state.SetTop(top);
return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status);
}

if (!MemoryRecordId.TryRead(state, -1, out parentId))
{
state.SetTop(top);
return NotAttempted(MemoryRecordMutationProblem.InvalidResult);
}

state.SetTop(top);
hasParent = true;
return Completed();
}

private static MemoryRecordMutationOutcome FromPreflightStatus(LuaStatus status)
{
return NotAttempted(MemoryRecordMutationProblem.LuaFailure, status);
Expand All @@ -502,8 +579,19 @@

private static MemoryRecordMutationOutcome Indeterminate(LuaStatus status)
{
return new MemoryRecordMutationOutcome(MemoryRecordMutationEffect.Indeterminate,
MemoryRecordMutationProblem.LuaFailure, status);
return Indeterminate(MemoryRecordMutationProblem.LuaFailure, status);
}

private static MemoryRecordMutationOutcome Indeterminate(MemoryRecordMutationProblem problem, LuaStatus status)
{
return new MemoryRecordMutationOutcome(MemoryRecordMutationEffect.Indeterminate, problem, status);
}

private static MemoryRecordMutationOutcome IndeterminateAfterParentSetter(MemoryRecordMutationOutcome outcome)
{
return outcome.Problem == MemoryRecordMutationProblem.LuaFailure
? Indeterminate(MemoryRecordMutationProblem.LuaFailure, outcome.LuaStatus)
: Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok);
}

// Progress of one SetActive command, shared with the catch block of its caller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ public enum MemoryRecordMutationEffect
/// <summary>No CE mutation was started.</summary>
NotAttempted,

/// <summary>The CE mutation completed successfully.</summary>
/// <summary>The CE mutation completed successfully, including any required postcondition read-back.</summary>
Completed,

/// <summary>The CE mutation started but returned a protected failure, so its final host state is unknown.</summary>
/// <summary>
/// The CE mutation started but returned a protected failure or left its postcondition unconfirmed, so its final
/// host state is unknown.
/// </summary>
Indeterminate
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ namespace CheatEngine.SDK.Engine.AddressList;

/// <summary>A structured result from a typed address-list record mutation.</summary>
/// <remarks>
/// <para>
/// The result describes the command only. It intentionally does not capture a record snapshot: callers that
/// need a post-command view must obtain a fresh snapshot after a
/// <see cref="MemoryRecordMutationEffect.Completed" />
/// result, and must not merge a snapshot-read failure with the mutation result.
/// </para>
/// <para>
/// <see cref="MemoryRecordMutationEffect.Indeterminate" /> means a protected call began and then failed. Do
/// not blindly retry it; CE may have applied part or all of the mutation before reporting the error.
/// </para>
/// <para>
/// The result describes the command only. It intentionally does not capture a record snapshot: callers that need a
/// post-command view must obtain a fresh snapshot after a <see cref="MemoryRecordMutationEffect.Completed" />
/// result, and must not merge a snapshot-read failure with the mutation result. Parent assignment is an exception to
/// the usual command-only shape: it reports <see cref="MemoryRecordMutationEffect.Completed" /> only after a fresh
/// child lookup confirms the requested parent identifier.
/// </para>
/// <para>
/// <see cref="MemoryRecordMutationEffect.Indeterminate" /> means a protected call started but either failed or left
/// its postcondition unconfirmed. Do not blindly retry it; CE may have applied part or all of the mutation before
/// reporting the error or invalid result.
/// </para>
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
public readonly struct MemoryRecordMutationOutcome
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ public enum MemoryRecordMutationProblem
/// <summary>The parent walk reached its explicit safety limit.</summary>
TraversalLimitReached,

/// <summary>A required CE global or member was absent or not callable.</summary>
/// <summary>
/// A required CE global or member was absent or not callable, or Cheat Engine exposes no safe operation for the
/// requested mutation (for example, detaching a nested record to the address-list root).
/// </summary>
GlobalUnavailable,

/// <summary>A protected CE access or call raised.</summary>
Expand Down
Loading
Loading