diff --git a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
index 721ed89a..6522f18c 100644
--- a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
+++ b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
@@ -100,9 +100,17 @@ public static MemoryRecordMutationOutcome Delete(MemoryRecordId recordId)
/// Assigns a record's parent after validating the requested hierarchy with the default traversal bound.
/// The child record's CE ID in the current list.
- /// The new parent's CE ID, or to make the child a root record.
+ ///
+ /// The new parent's CE ID, or to require that the child is already a root record.
+ ///
/// A command result that preserves whether assignment was started.
/// The plugin is not enabled or the calling thread has no Lua state.
+ ///
+ /// Cheat Engine exposes no safe Lua operation for detaching an existing nested record to the address-list root.
+ /// A request therefore completes only when the protected Parent read already
+ /// reports a root record; otherwise it is refused with
+ /// before the setter is called.
+ ///
[RequiresPluginEnabled]
public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, MemoryRecordId? parentId)
{
@@ -111,7 +119,9 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem
/// Assigns a record's parent after validating the requested hierarchy with .
/// The child record's CE ID in the current list.
- /// The new parent's CE ID, or to make the child a root record.
+ ///
+ /// The new parent's CE ID, or to require that the child is already a root record.
+ ///
/// A positive bound for the proposed parent's existing parent chain.
/// A command result that preserves whether assignment was started.
/// The plugin is not enabled or the calling thread has no Lua state.
@@ -139,30 +149,86 @@ public static MemoryRecordMutationOutcome SetParent(MemoryRecordId recordId, Mem
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)
+ {
+ 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)
+ {
+ return Indeterminate(MemoryRecordMutationProblem.InvalidResult, LuaStatus.Ok);
+ }
+
+ return Completed();
}
catch (LuaException exception)
{
@@ -335,38 +401,6 @@ private static MemoryRecordActivationOutcome Activation(MemoryRecordActivationOu
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)
{
@@ -479,6 +513,49 @@ private static MemoryRecordMutationOutcome ValidateParentChain(LuaState state, M
}
}
+ 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);
@@ -502,8 +579,19 @@ private static MemoryRecordMutationOutcome NotAttempted(MemoryRecordMutationProb
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.
diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs
index 87b93818..9bb0690e 100644
--- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs
+++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationEffect.cs
@@ -6,9 +6,12 @@ public enum MemoryRecordMutationEffect
/// No CE mutation was started.
NotAttempted,
- /// The CE mutation completed successfully.
+ /// The CE mutation completed successfully, including any required postcondition read-back.
Completed,
- /// The CE mutation started but returned a protected failure, so its final host state is unknown.
+ ///
+ /// The CE mutation started but returned a protected failure or left its postcondition unconfirmed, so its final
+ /// host state is unknown.
+ ///
Indeterminate
}
diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs
index 031b1b69..12e77953 100644
--- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs
+++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationOutcome.cs
@@ -6,16 +6,18 @@ namespace CheatEngine.SDK.Engine.AddressList;
/// A structured result from a typed address-list record mutation.
///
-///
-/// 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
-///
-/// result, and must not merge a snapshot-read failure with the mutation result.
-///
-///
-/// 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.
-///
+///
+/// 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
+/// 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 only after a fresh
+/// child lookup confirms the requested parent identifier.
+///
+///
+/// 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.
+///
///
[StructLayout(LayoutKind.Sequential)]
public readonly struct MemoryRecordMutationOutcome
diff --git a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs
index 7b71ff01..5c474518 100644
--- a/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs
+++ b/libs/CheatEngine.SDK.Engine/AddressList/MemoryRecordMutationProblem.cs
@@ -27,7 +27,10 @@ public enum MemoryRecordMutationProblem
/// The parent walk reached its explicit safety limit.
TraversalLimitReached,
- /// A required CE global or member was absent or not callable.
+ ///
+ /// 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).
+ ///
GlobalUnavailable,
/// A protected CE access or call raised.
diff --git a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs
index ee290090..9355b16b 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/AddressList/AddressListMutationsTests.cs
@@ -5,6 +5,7 @@
using CheatEngine.SDK.Engine.Tests.Support;
using CheatEngine.SDK.Lua.Calls;
using CheatEngine.SDK.Lua.Marshalling;
+using CheatEngine.SDK.Lua.Runtime;
using CheatEngine.SDK.Lua.State;
using CheatEngine.SDK.Tests.Shared.NativeLua;
@@ -124,19 +125,222 @@ public void Set_parent_obeys_an_explicit_parent_walk_bound()
}
[Fact]
- public void Set_parent_assigns_a_typed_parent_or_lua_nil_for_root()
+ public void Set_parent_verifies_the_fresh_parent_after_one_typed_assignment()
{
using MutationFixture fixture = new();
int top = fixture.State.Top;
MemoryRecordMutationOutcome parent =
AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2));
- fixture.Execute("assert(assigned == 1)");
- MemoryRecordMutationOutcome root = AddressListMutations.SetParent(new MemoryRecordId(1), null);
Assert.True(parent.IsCompleted);
- Assert.True(root.IsCompleted);
- fixture.Execute("assert(assigned == 2)");
+ fixture.RunOnChild("assert(o.props.Parent.ID == parent.ID)");
+ fixture.Execute("assert(assigned == 1)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Fact]
+ public void Set_parent_root_request_is_a_verified_no_op_or_an_unavailable_host_operation()
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+
+ MemoryRecordMutationOutcome alreadyRoot = AddressListMutations.SetParent(new MemoryRecordId(1), parentId: null);
+
+ Assert.True(alreadyRoot.IsCompleted);
+ fixture.RunOnChild("assert(o.props.Parent == nil)");
+ fixture.Execute("assert(assigned == 0)");
+
+ fixture.RunOnChild("o.props.Parent = parent");
+ MemoryRecordMutationOutcome nested = AddressListMutations.SetParent(new MemoryRecordId(1), parentId: null);
+
+ Assert.Equal(MemoryRecordMutationEffect.NotAttempted, nested.Effect);
+ Assert.Equal(MemoryRecordMutationProblem.GlobalUnavailable, nested.Problem);
+ // This setter models CE's silently ignored nil assignment. The SDK must never call it.
+ fixture.RunOnChild("assert(o.props.Parent.ID == parent.ID)");
+ fixture.Execute("assert(assigned == 0)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Fact]
+ public void Set_parent_root_request_reports_missing_child_without_calling_a_nil_setter()
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+
+ MemoryRecordMutationOutcome missing = AddressListMutations.SetParent(new MemoryRecordId(99), parentId: null);
+
+ Assert.Equal(MemoryRecordMutationEffect.NotAttempted, missing.Effect);
+ Assert.Equal(MemoryRecordMutationProblem.RecordNotFound, missing.Problem);
+ fixture.Execute("assert(assigned == 0)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Theory]
+ [InlineData("error('parent read failed')", MemoryRecordMutationProblem.LuaFailure)]
+ [InlineData("return 42", MemoryRecordMutationProblem.InvalidResult)]
+ public void Set_parent_root_request_preserves_read_failure_without_starting_assignment(string parentGetter,
+ MemoryRecordMutationProblem expectedProblem)
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ fixture.RunOnChild("o.getters.Parent = function() " + parentGetter + " end");
+
+ MemoryRecordMutationOutcome outcome = AddressListMutations.SetParent(new MemoryRecordId(1), parentId: null);
+
+ Assert.Equal(MemoryRecordMutationEffect.NotAttempted, outcome.Effect);
+ Assert.Equal(expectedProblem, outcome.Problem);
+ Assert.Equal(expectedProblem != MemoryRecordMutationProblem.LuaFailure, outcome.LuaStatus.IsOk);
+ fixture.RunOnChild("assert(o.props.Parent == nil)");
+ fixture.Execute("assert(assigned == 0)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Theory]
+ [InlineData(true, "error('parent ID read failed')", MemoryRecordMutationProblem.LuaFailure)]
+ [InlineData(true, "return 'not an ID'", MemoryRecordMutationProblem.InvalidResult)]
+ [InlineData(false, "error('parent ID read failed')", MemoryRecordMutationProblem.LuaFailure)]
+ [InlineData(false, "return 'not an ID'", MemoryRecordMutationProblem.InvalidResult)]
+ public void Set_parent_parent_id_read_failure_preserves_whether_assignment_started(bool rootRequest,
+ string idGetter, MemoryRecordMutationProblem expectedProblem)
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ string when = rootRequest ? "true" : "assigned > 0";
+ fixture.RunOnParent("o.getters.ID = function() if " + when + " then " + idGetter + " end; return 2 end");
+ if (rootRequest)
+ {
+ fixture.RunOnChild("o.props.Parent = parent");
+ }
+
+ MemoryRecordMutationOutcome outcome = AddressListMutations.SetParent(new MemoryRecordId(1),
+ rootRequest ? null : new MemoryRecordId(2));
+
+ Assert.Equal(rootRequest ? MemoryRecordMutationEffect.NotAttempted : MemoryRecordMutationEffect.Indeterminate,
+ outcome.Effect);
+ Assert.Equal(expectedProblem, outcome.Problem);
+ Assert.Equal(expectedProblem != MemoryRecordMutationProblem.LuaFailure, outcome.LuaStatus.IsOk);
+ fixture.RunOnParent("o.getters.ID = nil");
+ fixture.RunOnChild("assert(o.props.Parent.ID == 2)");
+ fixture.Execute(rootRequest ? "assert(assigned == 0)" : "assert(assigned == 1)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Theory]
+ [InlineData("assigned = assigned + 1")]
+ [InlineData("assigned = assigned + 1; o.props.Parent = grandparent")]
+ public void Set_parent_silent_or_wrong_host_result_is_indeterminate(string setterBody)
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ fixture.RunOnChild("o.setters.Parent = function(o, v) " + setterBody + " end");
+
+ MemoryRecordMutationOutcome outcome =
+ AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2));
+
+ Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect);
+ Assert.Equal(MemoryRecordMutationProblem.InvalidResult, outcome.Problem);
+ fixture.Execute("assert(assigned == 1)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Theory]
+ [InlineData("error('parent read failed')", MemoryRecordMutationProblem.LuaFailure)]
+ [InlineData("return 42", MemoryRecordMutationProblem.InvalidResult)]
+ public void Set_parent_post_setter_parent_read_failure_is_indeterminate(string parentGetter,
+ MemoryRecordMutationProblem expectedProblem)
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; o.getters.Parent = function() " + parentGetter + " end end");
+
+ MemoryRecordMutationOutcome outcome =
+ AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2));
+
+ Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect);
+ Assert.Equal(expectedProblem, outcome.Problem);
+ fixture.Execute("assert(assigned == 1)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Fact]
+ public void Set_parent_missing_post_setter_child_is_indeterminate()
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; records[1] = nil end");
+
+ MemoryRecordMutationOutcome outcome =
+ AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2));
+
+ Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect);
+ Assert.Equal(MemoryRecordMutationProblem.InvalidResult, outcome.Problem);
+ fixture.Execute("assert(assigned == 1)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Fact]
+ public void Set_parent_missing_post_setter_list_is_indeterminate()
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; address_list_lookup = function() return nil end end");
+
+ MemoryRecordMutationOutcome outcome =
+ AddressListMutations.SetParent(new MemoryRecordId(1), new MemoryRecordId(2));
+
+ Assert.Equal(MemoryRecordMutationEffect.Indeterminate, outcome.Effect);
+ Assert.Equal(MemoryRecordMutationProblem.InvalidResult, outcome.Problem);
+ fixture.Execute("assert(assigned == 1)");
+ Assert.Equal(top, fixture.State.Top);
+ }
+
+ [Theory]
+ [InlineData("preflight")]
+ [InlineData("setter")]
+ [InlineData("readback")]
+ [InlineData("root")]
+ public void Set_parent_admission_rejects_reentrant_resets_and_preserves_the_verified_result(string scenario)
+ {
+ using MutationFixture fixture = new();
+ int top = fixture.State.Top;
+ LuaStateIdentity identity = LuaRuntime.CurrentStateIdentity;
+ int resetAttempts = 0;
+ bool rootRequest = string.Equals(scenario, "root", StringComparison.Ordinal);
+ using (FakeHost.InstallReentrantHook(fixture.State, "attempt_reset", () =>
+ {
+ resetAttempts++;
+ FakeHost.ReplaceStateGeneration();
+ }))
+ {
+ switch (scenario)
+ {
+ case "preflight":
+ fixture.RunOnParent("o.getters.Parent = function() attempt_reset(); return nil end");
+ break;
+ case "setter":
+ fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; attempt_reset() end");
+ break;
+ case "readback":
+ fixture.RunOnChild("o.setters.Parent = function(o, v) assigned = assigned + 1; o.props.Parent = v; o.getters.Parent = function() attempt_reset(); return o.props.Parent end end");
+ break;
+ case "root":
+ fixture.RunOnChild("o.getters.Parent = function() attempt_reset(); return nil end");
+ break;
+ }
+
+ MemoryRecordMutationOutcome outcome = AddressListMutations.SetParent(new MemoryRecordId(1),
+ rootRequest ? null : new MemoryRecordId(2));
+
+ Assert.True(outcome.IsCompleted);
+ Assert.Equal(MemoryRecordMutationProblem.None, outcome.Problem);
+ }
+
+ Assert.IsType(FakeHost.TakeReentrantHookFailure());
+ Assert.Equal(1, resetAttempts);
+ Assert.Equal(identity, LuaRuntime.CurrentStateIdentity);
+ fixture.Execute(rootRequest ? "assert(assigned == 0)" : "assert(assigned == 1)");
+ fixture.RunOnChild(rootRequest ? "assert(o.props.Parent == nil)" : "assert(o.props.Parent.ID == parent.ID)");
Assert.Equal(top, fixture.State.Top);
}
@@ -188,24 +392,21 @@ public MutationFixture(bool parentSetterFails = false, bool destroyFails = false
CEObject listObject = FakeHost.CreateObject(State, "Probe");
string childInitializer = parentSetterFails
? "o.props.ID = 1; o.setters.Parent = function(o, v) o.props.Parent = v; assigned = assigned + 1; error('set after side effect') end"
- : "o.props.ID = 1; o.setters.Parent = function(o, v) o.props.Parent = v; assigned = assigned + 1 end";
- CEObject childObject = FakeHost.CreateObject(State, destroyFails ? "Stubborn" : "Probe", childInitializer);
- CEObject parentObject = FakeHost.CreateObject(State, "Probe", "o.props.ID = 2");
+ : "o.props.ID = 1; o.setters.Parent = function(o, v) assigned = assigned + 1; if v ~= nil then o.props.Parent = v end end";
+ ChildObject = FakeHost.CreateObject(State, destroyFails ? "Stubborn" : "Probe", childInitializer);
+ ParentObject = FakeHost.CreateObject(State, "Probe", "o.props.ID = 2");
CEObject grandparentObject = FakeHost.CreateObject(State, "Probe", "o.props.ID = 3");
SetGlobal(State, "list"u8, new EngineAddressList(listObject));
- SetGlobal(State, "child"u8, new MemoryRecord(childObject));
- SetGlobal(State, "parent"u8, new MemoryRecord(parentObject));
+ SetGlobal(State, "child"u8, new MemoryRecord(ChildObject));
+ SetGlobal(State, "parent"u8, new MemoryRecord(ParentObject));
SetGlobal(State, "grandparent"u8, new MemoryRecord(grandparentObject));
EngineTest.Run(State, """
destroyed = 0
assigned = 0
- function getAddressList() return list end
- list.getMemoryRecordByID = function(id)
- if id == 1 then return child end
- if id == 2 then return parent end
- if id == 3 then return grandparent end
- return nil
- end
+ address_list_lookup = function() return list end
+ function getAddressList() return address_list_lookup() end
+ records = { [1] = child, [2] = parent, [3] = grandparent }
+ list.getMemoryRecordByID = function(id) return records[id] end
child.destroy = function() destroyed = destroyed + 1 end
"""u8);
}
@@ -215,6 +416,16 @@ public LuaState State
get;
}
+ private CEObject ChildObject
+ {
+ get;
+ }
+
+ private CEObject ParentObject
+ {
+ get;
+ }
+
public void Dispose()
{
_scope.Dispose();
@@ -226,6 +437,16 @@ public void Execute(string source)
EngineTest.Run(State, Encoding.UTF8.GetBytes(source));
}
+ public void RunOnChild(string source)
+ {
+ FakeHost.RunOnObject(State, ChildObject, source);
+ }
+
+ public void RunOnParent(string source)
+ {
+ FakeHost.RunOnObject(State, ParentObject, source);
+ }
+
private static void SetGlobal(LuaState state, ReadOnlySpan name, T value)
where T : struct, ILuaMarshaller
{