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 {