diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34cf9e3e..3b211a54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/analyzers/CheatEngine.SDK.Analyzers/Diagnostics/DiagnosticDescriptors.cs b/analyzers/CheatEngine.SDK.Analyzers/Diagnostics/DiagnosticDescriptors.cs
index 372a8724..5a908203 100644
--- a/analyzers/CheatEngine.SDK.Analyzers/Diagnostics/DiagnosticDescriptors.cs
+++ b/analyzers/CheatEngine.SDK.Analyzers/Diagnostics/DiagnosticDescriptors.cs
@@ -9,226 +9,226 @@ namespace CheatEngine.SDK.Analyzers.Diagnostics;
///
internal static class DiagnosticDescriptors
{
- // One markdown page per rule, named after the identifier.
- private const string HelpLinkBase = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/";
-
- /// CESDK0001. Message arguments: the class name, then the sentence fragment describing the problem.
- public static readonly DiagnosticDescriptor InvalidPluginClass = new(
- DiagnosticIds.InvalidPluginClass,
- "Plugin class cannot be constructed by the generated entry point",
- "Plugin class '{0}' {1}",
- DiagnosticCategories.Plugin,
- DiagnosticSeverity.Error,
- true,
- "The entry point generated into a plugin assembly creates the plugin with 'new' from a top-level type of the same assembly. "
- + "The class marked [CheatEnginePlugin] must therefore be a non-static, non-abstract, non-generic class that is not nested in a generic type, "
- + "derives from CheatEngine.SDK.Hosting.Plugin.CheatEnginePlugin, is at least internal at every nesting level, is not file-local, does not take the reserved type name 'CESDK.CESDK', "
- + "has a public or internal parameterless constructor that 'new' can call without an object initializer (no required members left unset, no [Obsolete] error) "
- + "and carries a non-empty display name. When any of this is violated the generator emits nothing, or emits code that does not compile, and Cheat Engine cannot load the plugin.",
- HelpLinkBase + DiagnosticIds.InvalidPluginClass + ".md");
-
- /// CESDK0002 (compilation end). Message arguments: the class name, then the number of plugin classes found.
- public static readonly DiagnosticDescriptor MultiplePluginClasses = new(
- DiagnosticIds.MultiplePluginClasses,
- "More than one plugin class in the assembly",
- "'{0}' is one of {1} classes marked [CheatEnginePlugin]; a plugin assembly must contain exactly one, because Cheat Engine calls a single entry point per assembly",
- DiagnosticCategories.Plugin,
- DiagnosticSeverity.Error,
- true,
- "Cheat Engine loads one plugin per assembly through one fixed entry point. With several classes marked [CheatEnginePlugin] the generator cannot choose, "
- + "emits nothing, and Cheat Engine cannot load the plugin. Keep the attribute on exactly one class, or move the other plugins to their own assemblies.",
- HelpLinkBase + DiagnosticIds.MultiplePluginClasses + ".md",
- WellKnownDiagnosticTags.CompilationEnd);
-
- /// CESDK0003 (compilation end). Message argument: the missing manual-bootstrap requirement.
- public static readonly DiagnosticDescriptor InvalidManualBootstrap = new(
- DiagnosticIds.InvalidManualBootstrap,
- "Manual Cheat Engine bootstrap is missing or malformed",
- "Entry-point generation is disabled, but {0}",
- DiagnosticCategories.Plugin,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "When CheatEngineSdkGenerateEntryPoint is false, the assembly itself must provide the exact host entry point: "
- + "a static CESDK.CESDK type whose public static CEPluginInitialize(System.IntPtr, int) method returns int. "
- + "Cheat Engine looks up that identity by name and does not discover alternatives.",
- HelpLinkBase + DiagnosticIds.InvalidManualBootstrap + ".md",
- WellKnownDiagnosticTags.CompilationEnd);
-
- /// CESDK0004 (compilation end). Message argument: the declared namespace.
- public static readonly DiagnosticDescriptor ReservedNamespace = new(
- DiagnosticIds.ReservedNamespace,
- "Plugin assembly declares a namespace under 'CESDK'",
- "Namespace '{0}' is 'CESDK' or nested under it in a plugin assembly. Cheat Engine requires the type 'CESDK.CESDK' in every plugin assembly, "
- + "so inside that namespace the simple name 'CESDK' binds to that type and a name that starts with 'CESDK.' no longer reaches a namespace you declared under 'CESDK' (CS0426). Use a different root namespace.",
- DiagnosticCategories.Plugin,
- DiagnosticSeverity.Warning,
- true,
- "Cheat Engine requires the plugin assembly to contain the type 'CESDK.CESDK', which the entry point generator emits, so the assembly always holds a class named 'CESDK' in the namespace 'CESDK'. "
- + "Name lookup walks the enclosing namespaces before the global namespace: in code placed in 'CESDK' or 'CESDK.Something', the simple name 'CESDK' finds that class first, "
- + "so a qualified name that starts with 'CESDK.' no longer resolves to a namespace you declared under 'CESDK' (CS0426). Keep plugin code in a namespace that does not start with 'CESDK'. "
- + "The SDK itself lives under 'CheatEngine.SDK' and is not affected.",
- HelpLinkBase + DiagnosticIds.ReservedNamespace + ".md",
- WellKnownDiagnosticTags.CompilationEnd);
-
- /// CESDK0005 (compilation end). Message argument: the colliding source type.
- public static readonly DiagnosticDescriptor GeneratedEntryPointCollision = new(
- DiagnosticIds.GeneratedEntryPointCollision,
- "Source type collides with the generated Cheat Engine entry point",
- "Type '{0}' is declared by user code, but entry-point generation also emits CESDK.CESDK. Remove the type or disable generation and provide the complete manual bootstrap.",
- DiagnosticCategories.Plugin,
- DiagnosticSeverity.Error,
- true,
- "A generated plugin entry point always owns the type CESDK.CESDK. A user-authored type with that exact metadata "
- + "identity makes the compilation ambiguous or duplicate the host entry point. Either let the generator own it, "
- + "or disable generation and implement the complete manual bootstrap contract.",
- HelpLinkBase + DiagnosticIds.GeneratedEntryPointCollision + ".md",
- WellKnownDiagnosticTags.CompilationEnd);
-
- /// CESDK1001. Message argument: the enabled-only member called too early.
- public static readonly DiagnosticDescriptor RequiresPluginEnabledTooEarly = new(
- DiagnosticIds.RequiresPluginEnabledTooEarly,
- "Plugin startup code calls an enabled-only API",
- "'{0}' requires an enabled plugin and cannot be called from a plugin constructor, field initializer or property initializer",
- DiagnosticCategories.Usage,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "Cheat Engine attaches the SDK runtime only after constructing the plugin. An API marked RequiresPluginEnabled "
- + "therefore fails before OnEnable, including from instance construction and field or property initializers. Move the "
- + "operation into OnEnable or a method OnEnable calls.",
- HelpLinkBase + DiagnosticIds.RequiresPluginEnabledTooEarly + ".md");
-
- /// CESDK1003. Message argument: the directly borrowed expression.
- public static readonly DiagnosticDescriptor DisposeBorrowedValue = new(
- DiagnosticIds.DisposeBorrowedValue,
- "A Cheat Engine-owned value is being destroyed",
- "'{0}' is explicitly marked CEOwned (borrowed) and must not be disposed; only dispose an Owned<T> value you own",
- DiagnosticCategories.Usage,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "CEOwned marks a return value, property or parameter as a borrowed view of an object Cheat Engine owns. Calling "
- + "Dispose or DisposeAsync directly on that value can leave Cheat Engine with a dangling object. Keep it borrowed, "
- + "or obtain an explicit ownership-transfer contract before disposing it.",
- HelpLinkBase + DiagnosticIds.DisposeBorrowedValue + ".md");
-
- /// CESDK1004. Message argument: the method name.
- public static readonly DiagnosticDescriptor UnguardedUnmanagedCallersOnly = new(
- DiagnosticIds.UnguardedUnmanagedCallersOnly,
- "Exception can escape an [UnmanagedCallersOnly] method",
- "An exception can escape '{0}', which native code calls directly: the whole body must be one try statement whose catch-all clause returns a failure value",
- DiagnosticCategories.Usage,
- DiagnosticSeverity.Warning,
- true,
- "A managed exception that unwinds out of an [UnmanagedCallersOnly] method terminates the host process (Cheat Engine). "
- + "The rule is structural: every top-level statement of the body must be a try statement with a 'catch' or 'catch (System.Exception)' clause without a filter, "
- + "a local declaration or return whose value cannot throw, a local function declaration, an empty statement, or a nested block of such statements. "
- + "No catch or finally block of such a try statement may contain a throw or a call of a [DoesNotReturn] method other than Environment.FailFast and Environment.Exit.",
- HelpLinkBase + DiagnosticIds.UnguardedUnmanagedCallersOnly + ".md");
-
- /// CESDK1005. Message argument: the lifecycle method name.
- public static readonly DiagnosticDescriptor AsyncPluginLifecycle = new(
- DiagnosticIds.AsyncPluginLifecycle,
- "Plugin lifecycle callback must not be async void",
- "'{0}' is an async void lifecycle callback: its continuation can outlive the plugin enable or disable transition",
- DiagnosticCategories.Usage,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "Cheat Engine's enable and disable callbacks are synchronous and the host cannot await async void. A continuation may "
- + "run after teardown, lose exceptions, or touch an invalid Lua state. Keep OnEnable and OnDisable synchronous; use a "
- + "host-owned, explicitly tracked operation only when the API actually supports asynchronous waiting.",
- HelpLinkBase + DiagnosticIds.AsyncPluginLifecycle + ".md");
-
- /// CESDK2001. Message argument: the member name.
- public static readonly DiagnosticDescriptor UnsafeBlocksRequired = new(
- DiagnosticIds.UnsafeBlocksRequired,
- "Lua binding needs AllowUnsafeBlocks",
- "'{0}' is a Lua function export, but this compilation does not allow unsafe code (AllowUnsafeBlocks); registration thunks need it",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "The generated registration table takes the address of the UnmanagedCallersOnly thunks emitted for LuaFunction, which needs unsafe code. "
- + "LuaGlobal bodies do not take function addresses and therefore remain available without AllowUnsafeBlocks. A project that declares a LuaFunction must explicitly set true.",
- HelpLinkBase + DiagnosticIds.UnsafeBlocksRequired + ".md");
-
- /// CESDK2002. Message arguments: the member name, then the sentence fragment describing the problem.
- public static readonly DiagnosticDescriptor InvalidLuaBindingContainingType = new(
- DiagnosticIds.InvalidLuaBindingContainingType,
- "Type cannot receive a generated Lua binding part",
- "'{0}' is a Lua binding, but its containing type {1}",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- true,
- "The LuaBindings generator adds a generated part to the type that declares a [LuaFunction] or [LuaGlobal] member. "
- + "That type, and every type it is nested in, must be a partial, non-generic class or struct that is not a file-local type. "
- + "When any of this is violated the generator emits nothing for the member, and this rule names the cause.",
- HelpLinkBase + DiagnosticIds.InvalidLuaBindingContainingType + ".md");
-
- ///
- /// CESDK2003. Message arguments: the method name, then the sentence fragment describing the problem. All reports
- /// are local symbol diagnostics. CESDK2005 owns the one compilation-end condition, duplicate Lua names.
- ///
- public static readonly DiagnosticDescriptor InvalidLuaFunction = new(
- DiagnosticIds.InvalidLuaFunction,
- "[LuaFunction] method cannot be exported by a generated thunk",
- "Lua function '{0}' {1}",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- true,
- "A method marked [LuaFunction] must be an ordinary, static, non-generic, non-async method with a valid Lua name; parameters are passed by value, none optional or params, "
- + "each of a marshalled kind (int, long, float, double, bool, nuint, ReadOnlySpan, string) except an optional leading LuaState; the return type is void or one of the same marshalled kinds. "
- + "When any of this is violated the generator emits no thunk for the method, and this rule names the cause.",
- HelpLinkBase + DiagnosticIds.InvalidLuaFunction + ".md");
-
- /// CESDK2004. Message arguments: the method name, then the sentence fragment describing the problem.
- public static readonly DiagnosticDescriptor InvalidLuaGlobal = new(
- DiagnosticIds.InvalidLuaGlobal,
- "[LuaGlobal] method cannot receive a generated body",
- "Lua global binding '{0}' {1}",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- true,
- "A method marked [LuaGlobal] must be the defining declaration of an ordinary, static, non-generic, non-async partial method with a valid Lua name and no implementing part yet. "
- + "Parameters, in order: an optional leading LuaState, then by-value arguments of a marshalled kind, then the results (out parameters of a marshalled kind other than ReadOnlySpan, "
- + "or a copy-out pair Span destination, out int written). Any out result makes it the Try form, which must return bool; no result makes it the throwing form, "
- + "whose return type is void or a marshalled kind other than ReadOnlySpan. When any of this is violated the generator emits no body for the method, and this rule names the cause.",
- HelpLinkBase + DiagnosticIds.InvalidLuaGlobal + ".md");
-
- /// CESDK2005 (compilation end). Message arguments: the method and duplicated Lua name.
- public static readonly DiagnosticDescriptor DuplicateLuaName = new(
- DiagnosticIds.DuplicateLuaName,
- "Lua function name is duplicated",
- "Lua function '{0}' duplicates the Lua name '{1}' in the same containing type; one registration table cannot bind that name twice",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "Two valid LuaFunction methods of one type cannot export the same Lua global name. The generator deliberately emits "
- + "neither thunk so registration order cannot silently choose one. Give one method a distinct Lua name or move it to "
- + "another binding type.",
- HelpLinkBase + DiagnosticIds.DuplicateLuaName + ".md",
- WellKnownDiagnosticTags.CompilationEnd);
-
- /// CESDK2006. Message arguments: the annotated member and its unsupported shape.
- public static readonly DiagnosticDescriptor InvalidLuaAnnotationTarget = new(
- DiagnosticIds.InvalidLuaAnnotationTarget,
- "Lua annotation target cannot receive generated code",
- "Lua annotation on '{0}' cannot be generated: {1}",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "LuaClass, LuaMethod and LuaProperty are declarative generator inputs. Their target must have the exact partial "
- + "borrowed-handle or member shape that the generator can implement. The analyzer reports the invalid declaration at "
- + "its source location so an unsupported target never silently loses generated code.",
- HelpLinkBase + DiagnosticIds.InvalidLuaAnnotationTarget + ".md");
-
- /// CESDK2007. Message arguments: the member and the generated identity it collides with.
- public static readonly DiagnosticDescriptor GeneratedLuaIdentityCollision = new(
- DiagnosticIds.GeneratedLuaIdentityCollision,
- "User member collides with a generated Lua binding identity",
- "Member '{0}' collides with generated identity '{1}'; rename the user member or change the binding declaration",
- DiagnosticCategories.Generation,
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- "Generated Lua bindings add required members such as registration methods, thunk methods, cached globals and LuaClass "
- + "handle members. A source declaration with the same identity prevents compilation. This rule identifies the user "
- + "declaration before generated code is emitted.",
- HelpLinkBase + DiagnosticIds.GeneratedLuaIdentityCollision + ".md");
+ // One markdown page per rule, named after the identifier.
+ private const string HelpLinkBase = "https://github.com/CheatEngineNet/CheatEngine.SDK/blob/main/analyzers/docs/";
+
+ /// CESDK0001. Message arguments: the class name, then the sentence fragment describing the problem.
+ public static readonly DiagnosticDescriptor InvalidPluginClass = new(
+ DiagnosticIds.InvalidPluginClass,
+ "Plugin class cannot be constructed by the generated entry point",
+ "Plugin class '{0}' {1}",
+ DiagnosticCategories.Plugin,
+ DiagnosticSeverity.Error,
+ true,
+ "The entry point generated into a plugin assembly creates the plugin with 'new' from a top-level type of the same assembly. "
+ + "The class marked [CheatEnginePlugin] must therefore be a non-static, non-abstract, non-generic class that is not nested in a generic type, "
+ + "derives from CheatEngine.SDK.Hosting.Plugin.CheatEnginePlugin, is at least internal at every nesting level, is not file-local, does not take the reserved type name 'CESDK.CESDK', "
+ + "has a public or internal parameterless constructor that 'new' can call without an object initializer (no required members left unset, no [Obsolete] error) "
+ + "and carries a non-empty display name. When any of this is violated the generator emits nothing, or emits code that does not compile, and Cheat Engine cannot load the plugin.",
+ HelpLinkBase + DiagnosticIds.InvalidPluginClass + ".md");
+
+ /// CESDK0002 (compilation end). Message arguments: the class name, then the number of plugin classes found.
+ public static readonly DiagnosticDescriptor MultiplePluginClasses = new(
+ DiagnosticIds.MultiplePluginClasses,
+ "More than one plugin class in the assembly",
+ "'{0}' is one of {1} classes marked [CheatEnginePlugin]; a plugin assembly must contain exactly one, because Cheat Engine calls a single entry point per assembly",
+ DiagnosticCategories.Plugin,
+ DiagnosticSeverity.Error,
+ true,
+ "Cheat Engine loads one plugin per assembly through one fixed entry point. With several classes marked [CheatEnginePlugin] the generator cannot choose, "
+ + "emits nothing, and Cheat Engine cannot load the plugin. Keep the attribute on exactly one class, or move the other plugins to their own assemblies.",
+ HelpLinkBase + DiagnosticIds.MultiplePluginClasses + ".md",
+ WellKnownDiagnosticTags.CompilationEnd);
+
+ /// CESDK0003 (compilation end). Message argument: the missing manual-bootstrap requirement.
+ public static readonly DiagnosticDescriptor InvalidManualBootstrap = new(
+ DiagnosticIds.InvalidManualBootstrap,
+ "Manual Cheat Engine bootstrap is missing or malformed",
+ "Entry-point generation is disabled, but {0}",
+ DiagnosticCategories.Plugin,
+ DiagnosticSeverity.Error,
+ true,
+ "When CheatEngineSdkGenerateEntryPoint is false, the assembly itself must provide the exact host entry point: "
+ + "a static CESDK.CESDK type whose public static CEPluginInitialize(System.IntPtr, int) method returns int. "
+ + "Cheat Engine looks up that identity by name and does not discover alternatives.",
+ HelpLinkBase + DiagnosticIds.InvalidManualBootstrap + ".md",
+ WellKnownDiagnosticTags.CompilationEnd);
+
+ /// CESDK0004 (compilation end). Message argument: the declared namespace.
+ public static readonly DiagnosticDescriptor ReservedNamespace = new(
+ DiagnosticIds.ReservedNamespace,
+ "Plugin assembly declares a namespace under 'CESDK'",
+ "Namespace '{0}' is 'CESDK' or nested under it in a plugin assembly. Cheat Engine requires the type 'CESDK.CESDK' in every plugin assembly, "
+ + "so inside that namespace the simple name 'CESDK' binds to that type and a name that starts with 'CESDK.' no longer reaches a namespace you declared under 'CESDK' (CS0426). Use a different root namespace.",
+ DiagnosticCategories.Plugin,
+ DiagnosticSeverity.Warning,
+ true,
+ "Cheat Engine requires the plugin assembly to contain the type 'CESDK.CESDK', which the entry point generator emits, so the assembly always holds a class named 'CESDK' in the namespace 'CESDK'. "
+ + "Name lookup walks the enclosing namespaces before the global namespace: in code placed in 'CESDK' or 'CESDK.Something', the simple name 'CESDK' finds that class first, "
+ + "so a qualified name that starts with 'CESDK.' no longer resolves to a namespace you declared under 'CESDK' (CS0426). Keep plugin code in a namespace that does not start with 'CESDK'. "
+ + "The SDK itself lives under 'CheatEngine.SDK' and is not affected.",
+ HelpLinkBase + DiagnosticIds.ReservedNamespace + ".md",
+ WellKnownDiagnosticTags.CompilationEnd);
+
+ /// CESDK0005 (compilation end). Message argument: the colliding source type.
+ public static readonly DiagnosticDescriptor GeneratedEntryPointCollision = new(
+ DiagnosticIds.GeneratedEntryPointCollision,
+ "Source type collides with the generated Cheat Engine entry point",
+ "Type '{0}' is declared by user code, but entry-point generation also emits CESDK.CESDK. Remove the type or disable generation and provide the complete manual bootstrap.",
+ DiagnosticCategories.Plugin,
+ DiagnosticSeverity.Error,
+ true,
+ "A generated plugin entry point always owns the type CESDK.CESDK. A user-authored type with that exact metadata "
+ + "identity makes the compilation ambiguous or duplicate the host entry point. Either let the generator own it, "
+ + "or disable generation and implement the complete manual bootstrap contract.",
+ HelpLinkBase + DiagnosticIds.GeneratedEntryPointCollision + ".md",
+ WellKnownDiagnosticTags.CompilationEnd);
+
+ /// CESDK1001. Message argument: the enabled-only member called too early.
+ public static readonly DiagnosticDescriptor RequiresPluginEnabledTooEarly = new(
+ DiagnosticIds.RequiresPluginEnabledTooEarly,
+ "Plugin startup code calls an enabled-only API",
+ "'{0}' requires an enabled plugin and cannot be called from a plugin constructor, field initializer or property initializer",
+ DiagnosticCategories.Usage,
+ DiagnosticSeverity.Error,
+ true,
+ "Cheat Engine attaches the SDK runtime only after constructing the plugin. An API marked RequiresPluginEnabled "
+ + "therefore fails before OnEnable, including from instance construction and field or property initializers. Move the "
+ + "operation into OnEnable or a method OnEnable calls.",
+ HelpLinkBase + DiagnosticIds.RequiresPluginEnabledTooEarly + ".md");
+
+ /// CESDK1003. Message argument: the directly borrowed expression.
+ public static readonly DiagnosticDescriptor DisposeBorrowedValue = new(
+ DiagnosticIds.DisposeBorrowedValue,
+ "A Cheat Engine-owned value is being destroyed",
+ "'{0}' is explicitly marked CEOwned (borrowed) and must not be disposed; only dispose an Owned<T> value you own",
+ DiagnosticCategories.Usage,
+ DiagnosticSeverity.Error,
+ true,
+ "CEOwned marks a return value, property or parameter as a borrowed view of an object Cheat Engine owns. Calling "
+ + "Dispose or DisposeAsync directly on that value can leave Cheat Engine with a dangling object. Keep it borrowed, "
+ + "or obtain an explicit ownership-transfer contract before disposing it.",
+ HelpLinkBase + DiagnosticIds.DisposeBorrowedValue + ".md");
+
+ /// CESDK1004. Message argument: the method name.
+ public static readonly DiagnosticDescriptor UnguardedUnmanagedCallersOnly = new(
+ DiagnosticIds.UnguardedUnmanagedCallersOnly,
+ "Exception can escape an [UnmanagedCallersOnly] method",
+ "An exception can escape '{0}', which native code calls directly: the whole body must be one try statement whose catch-all clause returns a failure value",
+ DiagnosticCategories.Usage,
+ DiagnosticSeverity.Warning,
+ true,
+ "A managed exception that unwinds out of an [UnmanagedCallersOnly] method terminates the host process (Cheat Engine). "
+ + "The rule is structural: every top-level statement of the body must be a try statement with a 'catch' or 'catch (System.Exception)' clause without a filter, "
+ + "a local declaration or return whose value cannot throw, a local function declaration, an empty statement, or a nested block of such statements. "
+ + "No catch or finally block of such a try statement may contain a throw or a call of a [DoesNotReturn] method other than Environment.FailFast and Environment.Exit.",
+ HelpLinkBase + DiagnosticIds.UnguardedUnmanagedCallersOnly + ".md");
+
+ /// CESDK1005. Message argument: the lifecycle method name.
+ public static readonly DiagnosticDescriptor AsyncPluginLifecycle = new(
+ DiagnosticIds.AsyncPluginLifecycle,
+ "Plugin lifecycle callback must not be async void",
+ "'{0}' is an async void lifecycle callback: its continuation can outlive the plugin enable or disable transition",
+ DiagnosticCategories.Usage,
+ DiagnosticSeverity.Error,
+ true,
+ "Cheat Engine's enable and disable callbacks are synchronous and the host cannot await async void. A continuation may "
+ + "run after teardown, lose exceptions, or touch an invalid Lua state. Keep OnEnable and OnDisable synchronous; use a "
+ + "host-owned, explicitly tracked operation only when the API actually supports asynchronous waiting.",
+ HelpLinkBase + DiagnosticIds.AsyncPluginLifecycle + ".md");
+
+ /// CESDK2001. Message argument: the member name.
+ public static readonly DiagnosticDescriptor UnsafeBlocksRequired = new(
+ DiagnosticIds.UnsafeBlocksRequired,
+ "Lua binding needs AllowUnsafeBlocks",
+ "'{0}' is a Lua function export, but this compilation does not allow unsafe code (AllowUnsafeBlocks); registration thunks need it",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "The generated registration table takes the address of the UnmanagedCallersOnly thunks emitted for LuaFunction, which needs unsafe code. "
+ + "LuaGlobal bodies do not take function addresses and therefore remain available without AllowUnsafeBlocks. A project that declares a LuaFunction must explicitly set true.",
+ HelpLinkBase + DiagnosticIds.UnsafeBlocksRequired + ".md");
+
+ /// CESDK2002. Message arguments: the member name, then the sentence fragment describing the problem.
+ public static readonly DiagnosticDescriptor InvalidLuaBindingContainingType = new(
+ DiagnosticIds.InvalidLuaBindingContainingType,
+ "Type cannot receive a generated Lua binding part",
+ "'{0}' is a Lua binding, but its containing type {1}",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "The LuaBindings generator adds a generated part to the type that declares a [LuaFunction] or [LuaGlobal] member. "
+ + "That type, and every type it is nested in, must be a partial, non-generic class or struct that is not a file-local type. "
+ + "When any of this is violated the generator emits nothing for the member, and this rule names the cause.",
+ HelpLinkBase + DiagnosticIds.InvalidLuaBindingContainingType + ".md");
+
+ ///
+ /// CESDK2003. Message arguments: the method name, then the sentence fragment describing the problem. All reports
+ /// are local symbol diagnostics. CESDK2005 owns the one compilation-end condition, duplicate Lua names.
+ ///
+ public static readonly DiagnosticDescriptor InvalidLuaFunction = new(
+ DiagnosticIds.InvalidLuaFunction,
+ "[LuaFunction] method cannot be exported by a generated thunk",
+ "Lua function '{0}' {1}",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "A method marked [LuaFunction] must be an ordinary, static, non-generic, non-async method with a valid Lua name; parameters are passed by value, none optional or params, "
+ + "each of a marshalled kind (int, long, float, double, bool, nuint, ReadOnlySpan, string) except an optional leading LuaState; the return type is void or one of the same marshalled kinds. "
+ + "When any of this is violated the generator emits no thunk for the method, and this rule names the cause.",
+ HelpLinkBase + DiagnosticIds.InvalidLuaFunction + ".md");
+
+ /// CESDK2004. Message arguments: the method name, then the sentence fragment describing the problem.
+ public static readonly DiagnosticDescriptor InvalidLuaGlobal = new(
+ DiagnosticIds.InvalidLuaGlobal,
+ "[LuaGlobal] method cannot receive a generated body",
+ "Lua global binding '{0}' {1}",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "A method marked [LuaGlobal] must be the defining declaration of an ordinary, static, non-generic, non-async partial method with a valid Lua name and no implementing part yet. "
+ + "Parameters, in order: an optional leading LuaState, then by-value arguments of a marshalled kind, then the results (out parameters of a marshalled kind other than ReadOnlySpan, "
+ + "or a copy-out pair Span destination, out int written). Any out result makes it the Try form, which must return bool; no result makes it the throwing form, "
+ + "whose return type is void or a marshalled kind other than ReadOnlySpan. When any of this is violated the generator emits no body for the method, and this rule names the cause.",
+ HelpLinkBase + DiagnosticIds.InvalidLuaGlobal + ".md");
+
+ /// CESDK2005 (compilation end). Message arguments: the method and duplicated Lua name.
+ public static readonly DiagnosticDescriptor DuplicateLuaName = new(
+ DiagnosticIds.DuplicateLuaName,
+ "Lua function name is duplicated",
+ "Lua function '{0}' duplicates the Lua name '{1}' in the same containing type; one registration table cannot bind that name twice",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "Two valid LuaFunction methods of one type cannot export the same Lua global name. The generator deliberately emits "
+ + "neither thunk so registration order cannot silently choose one. Give one method a distinct Lua name or move it to "
+ + "another binding type.",
+ HelpLinkBase + DiagnosticIds.DuplicateLuaName + ".md",
+ WellKnownDiagnosticTags.CompilationEnd);
+
+ /// CESDK2006. Message arguments: the annotated member and its unsupported shape.
+ public static readonly DiagnosticDescriptor InvalidLuaAnnotationTarget = new(
+ DiagnosticIds.InvalidLuaAnnotationTarget,
+ "Lua annotation target cannot receive generated code",
+ "Lua annotation on '{0}' cannot be generated: {1}",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "LuaClass, LuaMethod and LuaProperty are declarative generator inputs. Their target must have the exact partial "
+ + "borrowed-handle or member shape that the generator can implement. The analyzer reports the invalid declaration at "
+ + "its source location so an unsupported target never silently loses generated code.",
+ HelpLinkBase + DiagnosticIds.InvalidLuaAnnotationTarget + ".md");
+
+ /// CESDK2007. Message arguments: the member and the generated identity it collides with.
+ public static readonly DiagnosticDescriptor GeneratedLuaIdentityCollision = new(
+ DiagnosticIds.GeneratedLuaIdentityCollision,
+ "User member collides with a generated Lua binding identity",
+ "Member '{0}' collides with generated identity '{1}'; rename the user member or change the binding declaration",
+ DiagnosticCategories.Generation,
+ DiagnosticSeverity.Error,
+ true,
+ "Generated Lua bindings add required members such as registration methods, thunk methods, cached globals and LuaClass "
+ + "handle members. A source declaration with the same identity prevents compilation. This rule identifies the user "
+ + "declaration before generated code is emitted.",
+ HelpLinkBase + DiagnosticIds.GeneratedLuaIdentityCollision + ".md");
}
diff --git a/libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs b/libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs
index 3e977645..58b0e44d 100644
--- a/libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs
+++ b/libs/CheatEngine.SDK.Abi/Native/ClassicDebugEventDispatcher.cs
@@ -21,7 +21,8 @@ namespace CheatEngine.SDK.Abi.Native;
/// ContinueDebugEvent cell is not yet a qualified SDK ABI surface.
///
///
-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;
@@ -152,64 +153,29 @@ internal static ClassicDebugEventRegistrationStatus TryRegister(
///
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)
@@ -228,6 +194,7 @@ private void CompleteRelease()
}
_callbacksDrained.Dispose();
+ return ClassicDebugEventReleaseStatus.Released;
}
private static int Dispatch(void* nativeEvent)
@@ -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;
@@ -255,7 +218,33 @@ private static int Dispatch(void* nativeEvent)
t_dispatcher = dispatcher;
try
{
- return InvokeHandler(dispatcher, handler, observations, nativeEvent);
+ DebugEventHeader header = Unsafe.ReadUnaligned(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)
{
@@ -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(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(
@@ -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)
{
@@ -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)
diff --git a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
index c218cc2e..78da8071 100644
--- a/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
+++ b/libs/CheatEngine.SDK.Engine/AddressList/AddressListMutations.cs
@@ -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);
@@ -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)
{
diff --git a/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs b/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs
index e1e90c39..6041aec6 100644
--- a/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs
+++ b/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs
@@ -99,6 +99,30 @@ internal static bool TryApplyCore(string script, [NotNullWhen(true)] out AutoAss
TargetSelection.CreateUnavailableCheck(targetObservation));
}
+ if (!TryApplyScript(script, state, out int disableInfoIndex))
+ {
+ patch = null;
+ return false;
+ }
+
+ // Retain the original table on the stack while the copy is rooted. A protected ref failure consumes only the
+ // copy and leaves the original table as the one remaining authority for a direct, target-checked disable.
+ LuaRef disableInfo = TrackDisableInfo(script, state, disableInfoIndex, disableInfoTracker, targetObservation);
+ try
+ {
+ patch = patchFactory(script, disableInfo, targetObservation.Incarnation.GetValueOrDefault());
+ return true;
+ }
+ catch (Exception exception)
+ {
+ TargetReleaseOutcome cleanupOutcome = CompensateFailedPublication(script, disableInfo,
+ targetObservation.Incarnation.GetValueOrDefault());
+ throw new EngineResourceHandoffException(ApplyOperation, cleanupOutcome, exception);
+ }
+ }
+
+ private static bool TryApplyScript(string script, LuaState state, out int disableInfoIndex)
+ {
PushAutoAssemble(state, ApplyOperation);
StringMarshaller.Push(state, script);
LuaStatus status = state.TryCall(1, 2);
@@ -114,7 +138,7 @@ internal static bool TryApplyCore(string script, [NotNullWhen(true)] out AutoAss
if (!state.ToBoolean(-2))
{
- patch = null;
+ disableInfoIndex = 0;
return false;
}
@@ -123,18 +147,23 @@ internal static bool TryApplyCore(string script, [NotNullWhen(true)] out AutoAss
ThrowUnexpectedResult(ApplyOperation, "a disable-info table on success", state.TypeOf(-1));
}
- // Retain the original table on the stack while the copy is rooted. A protected ref failure consumes only the
- // copy and leaves the original table as the one remaining authority for a direct, target-checked disable.
- int disableInfoIndex = state.AbsoluteIndex(-1);
+ disableInfoIndex = state.AbsoluteIndex(-1);
+ return true;
+ }
+
+ private static LuaRef TrackDisableInfo(string script, LuaState state, int disableInfoIndex,
+ AutoAssemblerDisableInfoTracker disableInfoTracker, TargetSelectionObservation targetObservation)
+ {
state.PushValue(disableInfoIndex);
- LuaRef disableInfo;
try
{
- disableInfo = disableInfoTracker(state);
+ LuaRef disableInfo = disableInfoTracker(state);
if (disableInfo is null)
{
throw new InvalidOperationException("The disable-info tracker returned no reference.");
}
+
+ return disableInfo;
}
catch (Exception exception)
{
@@ -143,18 +172,6 @@ internal static bool TryApplyCore(string script, [NotNullWhen(true)] out AutoAss
targetObservation.Incarnation.GetValueOrDefault());
throw new EngineResourceHandoffException(ApplyOperation, cleanupOutcome, exception);
}
-
- try
- {
- patch = patchFactory(script, disableInfo, targetObservation.Incarnation.GetValueOrDefault());
- return true;
- }
- catch (Exception exception)
- {
- TargetReleaseOutcome cleanupOutcome = CompensateFailedPublication(script, disableInfo,
- targetObservation.Incarnation.GetValueOrDefault());
- throw new EngineResourceHandoffException(ApplyOperation, cleanupOutcome, exception);
- }
}
// The owner always routes cleanup through this method. Keeping the LuaRef release in its finally block prevents a
diff --git a/libs/CheatEngine.SDK.Engine/Inspection/AddressResolutionOptions.cs b/libs/CheatEngine.SDK.Engine/Inspection/AddressResolutionOptions.cs
index bc8b3b5a..7681068e 100644
--- a/libs/CheatEngine.SDK.Engine/Inspection/AddressResolutionOptions.cs
+++ b/libs/CheatEngine.SDK.Engine/Inspection/AddressResolutionOptions.cs
@@ -1,24 +1,16 @@
-using System;
-
namespace CheatEngine.SDK.Engine.Inspection;
/// Controls the optional shallow argument of Cheat Engine's getAddressSafe.
///
/// Target and host symbol spaces have separate result types. is forwarded without managed
/// reinterpretation; always queries the target table and
-/// always queries the host table. The two-Boolean constructor
-/// and property remain only as an obsolete source and binary compatibility
-/// shape; passing that legacy host flag to is rejected so an old
-/// positional call cannot silently query a different address space.
+/// always queries the host table.
///
public readonly record struct AddressResolutionOptions
{
- /// Creates options while retaining the released two-Boolean constructor shape.
- public AddressResolutionOptions(bool UseHostSymbolTable = false, bool Shallow = false)
+ /// Creates options for the optional CE shallow argument.
+ public AddressResolutionOptions(bool Shallow = false)
{
- #pragma warning disable CS0618
- this.UseHostSymbolTable = UseHostSymbolTable;
- #pragma warning restore CS0618
this.Shallow = Shallow;
}
@@ -28,24 +20,4 @@ public bool Shallow
get;
init;
}
-
- /// Gets the removed CE local flag retained for source and binary compatibility.
- [Obsolete("Use EngineInspection.ResolveHostAddress for host-symbol resolution; ResolveAddress rejects this flag.",
- false)]
- public bool UseHostSymbolTable
- {
- get;
- init;
- }
-
- #pragma warning disable CS0618
- internal bool HostSymbolTableRequested => UseHostSymbolTable;
- #pragma warning restore CS0618
-
- /// Deconstructs the compatibility shape used by the released 1.0.0 API.
- public void Deconstruct(out bool UseHostSymbolTable, out bool Shallow)
- {
- UseHostSymbolTable = HostSymbolTableRequested;
- Shallow = this.Shallow;
- }
}
diff --git a/libs/CheatEngine.SDK.Engine/Inspection/EngineInspection.cs b/libs/CheatEngine.SDK.Engine/Inspection/EngineInspection.cs
index ab90da75..e081e23c 100644
--- a/libs/CheatEngine.SDK.Engine/Inspection/EngineInspection.cs
+++ b/libs/CheatEngine.SDK.Engine/Inspection/EngineInspection.cs
@@ -191,11 +191,6 @@ public static InspectionStatus ResolveAddress(SymbolExpression expression, Addre
out Address address)
{
ValidateSymbolExpression(expression);
- if (options.HostSymbolTableRequested)
- {
- throw new ArgumentException(
- "Host-symbol resolution must use EngineInspection.ResolveHostAddress.", nameof(options));
- }
using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation();
LuaState state = operation.State;
diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationHandoffException.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationHandoffException.cs
new file mode 100644
index 00000000..1464bdc0
--- /dev/null
+++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationHandoffException.cs
@@ -0,0 +1,46 @@
+using System;
+
+using CheatEngine.SDK.Engine.Errors;
+
+namespace CheatEngine.SDK.Engine.Inspection;
+
+///
+/// Represents a managed lease-publication failure after Cheat Engine registered a symbol and the SDK made its one
+/// permitted compensation attempt.
+///
+///
+/// The managed publication failure remains .
+/// separately records whether the single coordinator-qualified unregister was confirmed, could not begin, or
+/// remains indeterminate; this exception does not provide a way to retry it.
+///
+public sealed class SymbolRegistrationHandoffException : EngineException
+{
+ ///
+ /// Initializes a handoff failure with the result of the single compensation attempt and its managed publication
+ /// cause.
+ ///
+ /// The cleanup result observed after lease publication failed.
+ /// The managed failure that prevented lease publication.
+ public SymbolRegistrationHandoffException(SymbolRegistrationReleaseOutcome cleanupOutcome,
+ Exception? innerException)
+ : base(CreateMessage(cleanupOutcome), innerException)
+ {
+ CleanupOutcome = cleanupOutcome;
+ }
+
+ /// Gets the factual result of the one compensation attempt.
+ public SymbolRegistrationReleaseOutcome CleanupOutcome
+ {
+ get;
+ }
+
+ ///
+ public override EngineFailureKind Kind => EngineFailureKind.BindingFailure;
+
+ private static string CreateMessage(SymbolRegistrationReleaseOutcome cleanupOutcome)
+ {
+ return
+ "Cheat Engine registered a symbol before its managed cleanup lease could be published; compensation ended as " +
+ cleanupOutcome.Kind + ".";
+ }
+}
diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLeaseFactory.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLeaseFactory.cs
new file mode 100644
index 00000000..68f7ce70
--- /dev/null
+++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLeaseFactory.cs
@@ -0,0 +1,7 @@
+using CheatEngine.SDK.Lua.Runtime;
+
+namespace CheatEngine.SDK.Engine.Inspection;
+
+/// Internal lease-construction seam used only to test managed publication failures after CE registration.
+internal delegate SymbolRegistrationLease SymbolRegistrationLeaseFactory(SymbolName name,
+ SymbolRegistrationOptions options, LuaStateIdentity identity);
diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLeasePublisher.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLeasePublisher.cs
new file mode 100644
index 00000000..9adf0d95
--- /dev/null
+++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistrationLeasePublisher.cs
@@ -0,0 +1,4 @@
+namespace CheatEngine.SDK.Engine.Inspection;
+
+/// Internal lease-publication seam used only to test managed publication failures after CE registration.
+internal delegate void SymbolRegistrationLeasePublisher(SymbolName name, SymbolRegistrationLease lease);
diff --git a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs
index bdc64b9b..b77904e1 100644
--- a/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs
+++ b/libs/CheatEngine.SDK.Engine/Inspection/SymbolRegistry.cs
@@ -97,15 +97,33 @@ public static LuaOperationStatus Unregister(SymbolName name)
/// The protected registration status and a lease only on successful registration.
/// is default or otherwise invalid.
/// The plugin is not enabled or the calling thread has no Lua state.
+ ///
+ /// CE registered the name but managed lease construction or publication failed; the exception records the one
+ /// coordinator-qualified compensation attempt.
+ ///
///
/// The lease prevents older leases from unregistering a newer registration made through this coordinator. CE has
- /// no registration token, so this method makes no claim about replacements made outside that coordinator.
+ /// no registration token, so this method makes no claim about replacements made outside that coordinator. A
+ /// successful CE registration is compensated once if its managed lease cannot be published; callers must inspect
+ /// rather than retrying by name.
///
[RequiresPluginEnabled]
public static SymbolRegistrationAcquireOutcome TryRegisterOwned(SymbolName name, Address address,
SymbolRegistrationOptions options = default)
+ {
+ return TryRegisterOwnedCore(name, address, options, CreateLease, PublishLease);
+ }
+
+ // The seams are internal test infrastructure. They make the otherwise exceptional interval between a successful CE
+ // registration and managed lease publication deterministic, without allowing consumers to choose a different
+ // ownership or cleanup policy.
+ internal static SymbolRegistrationAcquireOutcome TryRegisterOwnedCore(SymbolName name, Address address,
+ SymbolRegistrationOptions options, SymbolRegistrationLeaseFactory leaseFactory,
+ SymbolRegistrationLeasePublisher leasePublisher)
{
ValidateName(name);
+ ArgumentNullException.ThrowIfNull(leaseFactory);
+ ArgumentNullException.ThrowIfNull(leasePublisher);
lock (SOwnedRegistrationGate)
{
LuaStateIdentity identity = LuaRuntime.CurrentStateIdentity;
@@ -127,9 +145,22 @@ public static SymbolRegistrationAcquireOutcome TryRegisterOwned(SymbolName name,
}
SupersedeCurrentLease(name);
- SymbolRegistrationLease lease = new(name, options, identity);
- SOwnedRegistrations.Add(name, lease);
- return new SymbolRegistrationAcquireOutcome(status, lease);
+ try
+ {
+ SymbolRegistrationLease lease = leaseFactory(name, options, identity);
+ if (lease is null)
+ {
+ throw new InvalidOperationException("The symbol-registration lease factory returned no lease.");
+ }
+
+ leasePublisher(name, lease);
+ return new SymbolRegistrationAcquireOutcome(status, lease);
+ }
+ catch (Exception exception)
+ {
+ SymbolRegistrationReleaseOutcome cleanupOutcome = CompensateFailedPublication(name, identity);
+ throw new SymbolRegistrationHandoffException(cleanupOutcome, exception);
+ }
}
}
@@ -153,10 +184,7 @@ internal static SymbolRegistrationReleaseOutcome ReleaseOwned(SymbolRegistration
if (!LuaRuntime.IsAttached || lease.Identity != LuaRuntime.CurrentStateIdentity)
{
- RemoveCurrentLease(lease);
- lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.StaleRuntime);
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime,
- LuaOperationStatus.Success);
+ return MarkStaleRuntime(lease);
}
if (!SOwnedRegistrations.TryGetValue(lease.Name, out SymbolRegistrationLease? current) ||
@@ -167,47 +195,63 @@ internal static SymbolRegistrationReleaseOutcome ReleaseOwned(SymbolRegistration
LuaOperationStatus.Success);
}
- LuaOperationStatus status;
- try
- {
- status = UnregisterCore(lease.Name);
- }
- catch (InvalidOperationException) when (!LuaRuntime.IsAttached ||
- lease.Identity != LuaRuntime.CurrentStateIdentity)
- {
- RemoveCurrentLease(lease);
- lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.StaleRuntime);
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime,
- LuaOperationStatus.Success);
- }
- catch (InvalidOperationException)
- {
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable,
- LuaOperationStatus.GlobalUnavailable);
- }
-
- if (status.IsSuccess)
- {
- RemoveCurrentLease(lease);
- if (lease.Identity != LuaRuntime.CurrentStateIdentity)
- {
- lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.StaleRuntime);
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime, status);
- }
+ return ReleaseCurrentLease(lease);
+ }
+ }
- lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.Released);
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.Released, status);
- }
+ private static SymbolRegistrationReleaseOutcome ReleaseCurrentLease(SymbolRegistrationLease lease)
+ {
+ LuaOperationStatus status;
+ try
+ {
+ status = UnregisterCore(lease.Name);
+ }
+ catch (InvalidOperationException) when (!LuaRuntime.IsAttached ||
+ lease.Identity != LuaRuntime.CurrentStateIdentity)
+ {
+ return MarkStaleRuntime(lease);
+ }
+ catch (InvalidOperationException)
+ {
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable,
+ LuaOperationStatus.GlobalUnavailable);
+ }
+ catch (Exception)
+ {
+ RemoveCurrentLease(lease);
+ lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.CleanupIndeterminate);
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupIndeterminate,
+ LuaOperationStatus.InvalidResult);
+ }
- if (status.Kind is LuaOperationStatusKind.GlobalUnavailable or LuaOperationStatusKind.StackUnavailable)
+ if (status.IsSuccess)
+ {
+ RemoveCurrentLease(lease);
+ if (lease.Identity != LuaRuntime.CurrentStateIdentity)
{
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable, status);
+ return MarkStaleRuntime(lease, status);
}
- RemoveCurrentLease(lease);
- lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.CleanupIndeterminate);
- return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupIndeterminate, status);
+ lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.Released);
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.Released, status);
}
+
+ if (status.Kind is LuaOperationStatusKind.GlobalUnavailable or LuaOperationStatusKind.StackUnavailable)
+ {
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable, status);
+ }
+
+ RemoveCurrentLease(lease);
+ lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.CleanupIndeterminate);
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupIndeterminate, status);
+ }
+
+ private static SymbolRegistrationReleaseOutcome MarkStaleRuntime(SymbolRegistrationLease lease,
+ LuaOperationStatus status = default)
+ {
+ RemoveCurrentLease(lease);
+ lease.MarkTerminalAndObserve(SymbolRegistrationReleaseKind.StaleRuntime);
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime, status);
}
private static void ValidateName(SymbolName name)
@@ -218,6 +262,63 @@ private static void ValidateName(SymbolName name)
}
}
+ private static SymbolRegistrationLease CreateLease(SymbolName name, SymbolRegistrationOptions options,
+ LuaStateIdentity identity)
+ {
+ return new SymbolRegistrationLease(name, options, identity);
+ }
+
+ private static void PublishLease(SymbolName name, SymbolRegistrationLease lease)
+ {
+ SOwnedRegistrations.Add(name, lease);
+ }
+
+ // Publication runs while the coordinator gate is held, so an SDK-coordinated replacement cannot interleave between
+ // the successful register and this one compensation attempt. The identity check prevents an old registration from
+ // being unregistered through a replacement Lua runtime.
+ private static SymbolRegistrationReleaseOutcome CompensateFailedPublication(SymbolName name,
+ LuaStateIdentity identity)
+ {
+ if (!LuaRuntime.IsAttached || identity != LuaRuntime.CurrentStateIdentity)
+ {
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime,
+ LuaOperationStatus.Success);
+ }
+
+ LuaOperationStatus status;
+ try
+ {
+ status = UnregisterCore(name);
+ }
+ catch (InvalidOperationException) when (!LuaRuntime.IsAttached ||
+ identity != LuaRuntime.CurrentStateIdentity)
+ {
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime,
+ LuaOperationStatus.Success);
+ }
+ catch (InvalidOperationException)
+ {
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable,
+ LuaOperationStatus.GlobalUnavailable);
+ }
+ catch (Exception)
+ {
+ return new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupIndeterminate,
+ LuaOperationStatus.InvalidResult);
+ }
+
+ if (status.IsSuccess)
+ {
+ return identity == LuaRuntime.CurrentStateIdentity
+ ? new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.Released, status)
+ : new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.StaleRuntime, status);
+ }
+
+ return status.Kind is LuaOperationStatusKind.GlobalUnavailable or LuaOperationStatusKind.StackUnavailable
+ ? new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupUnavailable, status)
+ : new SymbolRegistrationReleaseOutcome(SymbolRegistrationReleaseKind.CleanupIndeterminate, status);
+ }
+
private static void SupersedeCurrentLease(SymbolName name)
{
if (!SOwnedRegistrations.Remove(name, out SymbolRegistrationLease? existing))
diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationOutcome.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationOutcome.cs
new file mode 100644
index 00000000..b0d7c029
--- /dev/null
+++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationOutcome.cs
@@ -0,0 +1,34 @@
+using System.Runtime.InteropServices;
+
+using CheatEngine.SDK.Engine.Targets;
+
+namespace CheatEngine.SDK.Engine.Scanning.Values;
+
+///
+/// A factual creation result for a target-dependent memory-scan session, including the target observation made
+/// before either CE factory can acquire an owned object.
+///
+[StructLayout(LayoutKind.Auto)]
+public readonly record struct MemoryScanCreationOutcome
+{
+ internal MemoryScanCreationOutcome(MemoryScanCreationStatus status, TargetSelectionObservation targetObservation)
+ {
+ Status = status;
+ TargetObservation = targetObservation;
+ }
+
+ /// Gets the stable factory result category.
+ public MemoryScanCreationStatus Status
+ {
+ get;
+ }
+
+ ///
+ /// Gets the target observation made before scanner acquisition. Its facts are retained even when creation was
+ /// refused because they do not qualify a target incarnation.
+ ///
+ public TargetSelectionObservation TargetObservation
+ {
+ get;
+ }
+}
diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationStatus.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationStatus.cs
index f4409bb2..de0b12f8 100644
--- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationStatus.cs
+++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanCreationStatus.cs
@@ -32,5 +32,10 @@ public enum MemoryScanCreationStatus : byte
AliasedFoundList = 7,
/// A rollback destroy call began but Cheat Engine did not confirm all required cleanup operations.
- RollbackUnconfirmed = 8
+ RollbackUnconfirmed = 8,
+
+ ///
+ /// The selected target could not be qualified before either target-dependent CE factory was invoked.
+ ///
+ TargetIdentityUnavailable = 9
}
diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanMaterializationStatus.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanMaterializationStatus.cs
index 88371597..47c592a1 100644
--- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanMaterializationStatus.cs
+++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanMaterializationStatus.cs
@@ -28,5 +28,8 @@ public enum MemoryScanMaterializationStatus : byte
LuaFailure = 7,
/// CE returned a count, address, or value that does not satisfy the declared scan contract.
- InvalidResult = 8
+ InvalidResult = 8,
+
+ /// The requested page starts at or beyond a non-empty found-list count.
+ PageStartOutOfRange = 9
}
diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanReleaseOutcome.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanReleaseOutcome.cs
new file mode 100644
index 00000000..d0e18778
--- /dev/null
+++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanReleaseOutcome.cs
@@ -0,0 +1,55 @@
+using System.Runtime.InteropServices;
+
+using CheatEngine.SDK.Engine.Targets;
+
+namespace CheatEngine.SDK.Engine.Scanning.Values;
+
+///
+/// The stable outcome of consuming a memory-scan session's child owner before its parent
+/// owner.
+///
+///
+/// Each owner is consumed exactly once. The two values distinguish confirmed
+/// cleanup, a safe refusal before the CE destroy call, cleanup that could not begin, and a native effect that began
+/// but could not be confirmed. A later release or returns this same outcome
+/// and never retries either CE destroy call.
+///
+[StructLayout(LayoutKind.Auto)]
+public readonly record struct MemoryScanReleaseOutcome
+{
+ internal MemoryScanReleaseOutcome(TargetReleaseOutcome foundList, TargetReleaseOutcome memScan,
+ bool foundListOwnershipConsumed, bool memScanOwnershipConsumed)
+ {
+ FoundList = foundList;
+ MemScan = memScan;
+ FoundListOwnershipConsumed = foundListOwnershipConsumed;
+ MemScanOwnershipConsumed = memScanOwnershipConsumed;
+ }
+
+ /// Gets the child found-list cleanup result, which is always processed before .
+ public TargetReleaseOutcome FoundList
+ {
+ get;
+ }
+
+ /// Gets the parent scanner cleanup result.
+ public TargetReleaseOutcome MemScan
+ {
+ get;
+ }
+
+ /// Gets whether the session consumed its found-list ownership capability.
+ public bool FoundListOwnershipConsumed
+ {
+ get;
+ }
+
+ /// Gets whether the session consumed its scanner ownership capability.
+ public bool MemScanOwnershipConsumed
+ {
+ get;
+ }
+
+ /// Gets whether both session ownership capabilities have been consumed.
+ public bool OwnershipConsumed => FoundListOwnershipConsumed && MemScanOwnershipConsumed;
+}
diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs
index 88e3fb46..2a21df26 100644
--- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs
+++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSession.cs
@@ -5,6 +5,7 @@
using CheatEngine.SDK.Annotations.Lifetime;
using CheatEngine.SDK.Annotations.Threading;
using CheatEngine.SDK.Engine.Enums;
+using CheatEngine.SDK.Engine.Errors;
using CheatEngine.SDK.Engine.Objects;
using CheatEngine.SDK.Engine.Targets;
using CheatEngine.SDK.Engine.Values;
@@ -110,6 +111,16 @@ public MemoryScanCancellationMilestone LastCancellationMilestone
private set;
}
+ ///
+ /// Gets the stable outcome of the one child-before-parent release attempt, or an unspecified outcome before the
+ /// session has been released or abandoned.
+ ///
+ public MemoryScanReleaseOutcome LastReleaseOutcome
+ {
+ get;
+ private set;
+ }
+
///
/// Gets the scanner as a borrowed handle. Direct raw operations on this value bypass the session's state checks;
/// prefer the session members for the scan lifecycle.
@@ -170,71 +181,59 @@ public ulong ResultCount
}
}
- ///
- /// Releases the readable list when necessary, then destroys the owned found-list child before the owned scanner
- /// parent. Never retries a destruction and is safe to call more than once.
- ///
+ /// Best-effort, no-throw disposal that consumes both owners and never implicitly retries CE cleanup.
///
- /// A different runtime identity or target incarnation marks the session
- /// with its before this method throws without releasing either owner. A merely
- /// unavailable target leaves the current state intact. A caller may retry only while the original context remains
- /// current; otherwise it must explicitly the managed owners. Once destruction begins, it
- /// follows and does not retry a protected CE failure.
+ /// Use when the factual child and parent outcomes matter. This method is
+ /// idempotent; it performs neither a target-dependent destroy on a worker nor a retry after an uncertain native
+ /// destroy. It records safe refusal or an unconfirmed result in instead.
///
- ///
- /// The plugin is attached and the caller is not on Cheat Engine's main thread.
- ///
- // Attached-worker cleanup is unsafe. The documented exception preserves both owners so disposal can be retried on
- // the main thread; making IDisposable.Dispose non-throwing here would either leak them or violate thread affinity.
-#pragma warning disable S3877
[MainThreadOnly]
public void Dispose()
+ {
+ _ = ReleaseWithOutcome();
+ }
+
+ ///
+ /// Consumes the found-list child and scanner parent in that order and returns both one-shot cleanup outcomes.
+ ///
+ ///
+ /// A stable result that identifies consumed ownership independently from confirmed, refused, unavailable, or
+ /// unconfirmed cleanup. The same result is returned after the session is already disposed.
+ ///
+ ///
+ /// This method never throws and never retries a destroy. If cleanup cannot safely begin (for example, a worker
+ /// thread, detached runtime, changed Lua identity, or changed target), it consumes the managed owners through
+ /// and reports the refusal or unavailable cleanup rather than routing handles into
+ /// a different CE context. A protected destroy failure consumes the corresponding owner and remains unconfirmed.
+ ///
+ public MemoryScanReleaseOutcome ReleaseWithOutcome()
{
if (State == MemoryScanState.Disposed)
{
- return;
+ return LastReleaseOutcome;
}
- // Detached or reattached cleanup cannot prove that this is the original Lua/target context, so context
- // validation below preserves both owners and requires the explicit Abandon recovery path. An attached worker
- // thread is different: attempting CE cleanup there is unsafe, so reject it before touching the Lua stack.
- if (LuaRuntime.IsAttached && !LuaRuntime.IsMainThread)
+ try
{
- throw new InvalidOperationException(
- "Memory scan disposal must run on Cheat Engine's main thread while the plugin is attached.");
- }
-
- // Admit the complete cleanup before publishing any lifetime change. Owned retains an owner when it cannot
- // begin destroy(), so this session must retain both owners in that case as well.
- using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation();
- LuaState state = operation.State;
- EnsureCurrentContext(state, "MemoryScan.Dispose");
- Owned foundList = _foundList!;
- Owned scanner = _scanner!;
+ if (LuaRuntime.IsAttached && !LuaRuntime.IsMainThread)
+ {
+ return ConsumeWithoutCleanup(TargetReleaseOutcome.NotInvoked(EngineFailureKind.BindingFailure));
+ }
- if (State == MemoryScanState.ResultsReady)
- {
- using LuaFrame frame = new(state);
- _ = foundList.Value.Handle.TryCallMethod(state, "deinitialize"u8, 0, 0);
- }
+ using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation();
+ MemoryScanMaterializationStatus context = TryEnsureCurrentContext(operation.State);
+ if (context != MemoryScanMaterializationStatus.Success)
+ {
+ return ConsumeWithoutCleanup(CreateRefusedReleaseOutcome(context));
+ }
- // Each frame removes a protected-call error before the next destroy. TryDestroy consumes an owner only once its
- // protected invocation begins; the outer admitted operation keeps the binding stable for both child and parent.
- using (LuaFrame frame = new(state))
- {
- _ = foundList.TryDestroy(state);
+ return ReleaseWithinCurrentContext(operation.State);
}
-
- using (LuaFrame frame = new(state))
+ catch (Exception)
{
- _ = scanner.TryDestroy(state);
+ return ConsumeWithoutCleanup(TargetReleaseOutcome.NotInvoked(EngineFailureKind.BindingFailure));
}
-
- _foundList = null;
- _scanner = null;
- State = MemoryScanState.Disposed;
}
-#pragma warning restore S3877
///
/// Stops managed cleanup without invoking CE and makes this session unusable.
@@ -251,19 +250,7 @@ public void Abandon()
return;
}
- if (_foundList is not null && !_foundList.IsDisposed)
- {
- _ = _foundList.Abandon();
- }
-
- if (_scanner is not null && !_scanner.IsDisposed)
- {
- _ = _scanner.Abandon();
- }
-
- _foundList = null;
- _scanner = null;
- State = MemoryScanState.Disposed;
+ _ = ConsumeWithoutCleanup(TargetReleaseOutcome.NotInvoked());
}
///
@@ -285,8 +272,15 @@ public static MemoryScanSession Adopt(Owned scanner, Owned f
{
RequireEnabledMainThread();
using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation();
+ MemoryScanSessionContext context = MemoryScanSessionContext.Capture(operation.State);
+ if (!context.TargetObservation.IsQualified)
+ {
+ throw new MemoryScanException(MemoryScanFailureKind.TargetIdentityUnavailable, "MemoryScan.Adopt",
+ "The memory scan session cannot adopt target-dependent owners without a qualified target incarnation.");
+ }
+
MemoryScanSession session = AdoptUnbound(scanner, foundList);
- session.Bind(MemoryScanSessionContext.Capture(operation.State));
+ session.Bind(context);
return session;
}
@@ -623,7 +617,7 @@ public MemoryScanMaterializationStatus TryCopyResultsCancellable(Span
+ /// Copies one caller-bounded page of initialized results without allocating storage for the complete found list.
+ ///
+ /// The zero-based index of the first row requested for this page.
+ /// The caller-owned maximum page storage.
+ /// The complete CE row count when it was read successfully; otherwise zero.
+ /// The page row count, zero unless the returned status is successful.
+ /// A success, empty-result, page-boundary, capacity, cancellation, context, Lua, or malformed-result category.
+ /// is negative.
+ ///
+ /// The temporary staging array is limited to this page, never the full count. No page prefix is copied to
+ /// if cancellation is observed or any page row is malformed.
+ ///
+ [MainThreadOnly]
+ [RequiresPluginEnabled]
+ public MemoryScanMaterializationStatus TryCopyResultsPage(int firstResultIndex, Span destination,
+ out ulong totalCount, out int written)
+ {
+ return TryCopyResultsPageCancellable(firstResultIndex, destination, out totalCount, out written,
+ CancellationToken.None);
+ }
+
+ ///
+ /// Copies one caller-bounded result page while observing cancellation between synchronous CE row calls.
+ ///
+ /// The zero-based index of the first row requested for this page.
+ /// The caller-owned maximum page storage.
+ /// The complete CE row count when it was read successfully; otherwise zero.
+ /// The page row count, zero unless the returned status is successful.
+ /// A cooperative cancellation token; it cannot interrupt a CE row call already begun.
+ /// A success, empty-result, page-boundary, capacity, cancellation, context, Lua, or malformed-result category.
+ /// is negative.
+ ///
+ /// This operation stages at most .Length rows, then publishes the page only when
+ /// every staged address/value pair is valid and cancellation has not been observed.
+ ///
+ [MainThreadOnly]
+ [RequiresPluginEnabled]
+ public MemoryScanMaterializationStatus TryCopyResultsPageCancellable(int firstResultIndex,
+ Span destination, out ulong totalCount, out int written, CancellationToken cancellationToken)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(firstResultIndex);
+ totalCount = 0;
+ written = 0;
+ LastCancellationMilestone = MemoryScanCancellationMilestone.None;
+ RequireEnabledMainThread();
+ if (State != MemoryScanState.ResultsReady)
+ {
+ ThrowWrongState("TryCopyResultsPage");
+ }
+
+ using LuaRuntimeOperation operation = LuaRuntime.AcquireOperation();
+ MemoryScanMaterializationStatus context = TryEnsureCurrentContext(operation.State);
+ if (context != MemoryScanMaterializationStatus.Success)
+ {
+ return context;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ LastCancellationMilestone = MemoryScanCancellationMilestone.CancelledBeforeNativeCall;
+ return MemoryScanMaterializationStatus.Cancelled;
+ }
+
+ FoundList foundList = RequireResults();
+ try
+ {
+ totalCount = ReadResultCount(operation.State, foundList);
+ if (totalCount == 0)
{
- LastCancellationMilestone = MemoryScanCancellationMilestone.ObservedAfterNativeCall;
- return MemoryScanMaterializationStatus.Cancelled;
+ return MemoryScanMaterializationStatus.NoResults;
+ }
+
+ if ((ulong) firstResultIndex >= totalCount)
+ {
+ return MemoryScanMaterializationStatus.PageStartOutOfRange;
+ }
+
+ if (destination.IsEmpty)
+ {
+ return MemoryScanMaterializationStatus.DestinationTooSmall;
+ }
+
+ int pageLength = (int) Math.Min((ulong) destination.Length, totalCount - (ulong) firstResultIndex);
+ MemoryScanResult[] snapshot = new MemoryScanResult[pageLength];
+ MemoryScanMaterializationStatus status = TryFillSnapshot(operation.State, foundList, snapshot,
+ firstResultIndex, cancellationToken);
+ if (status != MemoryScanMaterializationStatus.Success)
+ {
+ return status;
}
snapshot.AsSpan().CopyTo(destination);
@@ -682,11 +759,138 @@ public MemoryScanMaterializationStatus TryCopyResultsCancellable(Span MemoryScanMaterializationStatus.LuaFailure,
- _ => MemoryScanMaterializationStatus.InvalidResult
- };
+ LastCancellationMilestone = MemoryScanCancellationMilestone.ObservedAfterNativeCall;
+ return MemoryScanMaterializationStatus.Cancelled;
+ }
+
+ int index = firstResultIndex + offset;
+ string addressText = CallString(state, foundList.Handle, "getAddress"u8, ResultAddressOperation, index);
+ if (cancellationToken.IsCancellationRequested)
+ {
+ LastCancellationMilestone = MemoryScanCancellationMilestone.ObservedAfterNativeCall;
+ return MemoryScanMaterializationStatus.Cancelled;
+ }
+
+ if (!Address.TryParse(addressText, out Address address))
+ {
+ return MemoryScanMaterializationStatus.InvalidResult;
+ }
+
+ string value = CallString(state, foundList.Handle, "getValue"u8, ResultValueOperation, index);
+ snapshot[offset] = new MemoryScanResult(address, value);
+ }
+
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ return MemoryScanMaterializationStatus.Success;
+ }
+
+ LastCancellationMilestone = MemoryScanCancellationMilestone.ObservedAfterNativeCall;
+ return MemoryScanMaterializationStatus.Cancelled;
+ }
+
+ private static MemoryScanMaterializationStatus ToMaterializationStatus(MemoryScanException exception)
+ {
+ return exception.FailureKind == MemoryScanFailureKind.LuaError
+ ? MemoryScanMaterializationStatus.LuaFailure
+ : MemoryScanMaterializationStatus.InvalidResult;
+ }
+
+ private MemoryScanReleaseOutcome ReleaseWithinCurrentContext(LuaState state)
+ {
+ Owned? foundList = _foundList;
+ Owned? scanner = _scanner;
+ if (State == MemoryScanState.ResultsReady && foundList is not null && !foundList.IsDisposed)
+ {
+ using LuaFrame frame = new(state);
+ _ = foundList.Value.Handle.TryCallMethod(state, "deinitialize"u8, 0, 0);
+ }
+
+ TargetReleaseOutcome foundListOutcome = ReleaseOwned(state, foundList);
+ TargetReleaseOutcome scannerOutcome = ReleaseOwned(state, scanner);
+ return CompleteRelease(foundListOutcome, scannerOutcome);
+ }
+
+ private MemoryScanReleaseOutcome ConsumeWithoutCleanup(TargetReleaseOutcome outcome)
+ {
+ ConsumeOwner(_foundList);
+ ConsumeOwner(_scanner);
+ return CompleteRelease(outcome, outcome);
+ }
+
+ private MemoryScanReleaseOutcome CompleteRelease(TargetReleaseOutcome foundListOutcome,
+ TargetReleaseOutcome scannerOutcome)
+ {
+ _foundList = null;
+ _scanner = null;
+ State = MemoryScanState.Disposed;
+ LastReleaseOutcome = new MemoryScanReleaseOutcome(foundListOutcome, scannerOutcome, true, true);
+ return LastReleaseOutcome;
+ }
+
+ private TargetReleaseOutcome CreateRefusedReleaseOutcome(MemoryScanMaterializationStatus context)
+ {
+ if (context is MemoryScanMaterializationStatus.TargetIdentityUnavailable or
+ MemoryScanMaterializationStatus.TargetIdentityMismatch && LastTargetCheck.HasValue)
+ {
+ return TargetReleaseOutcome.Refused(LastTargetCheck.GetValueOrDefault());
+ }
+
+ return TargetReleaseOutcome.NotInvoked(context == MemoryScanMaterializationStatus.RuntimeInvalidated
+ ? EngineFailureKind.BindingFailure
+ : EngineFailureKind.TargetIdentityUnavailable);
+ }
+
+ private static TargetReleaseOutcome ReleaseOwned(LuaState state, Owned? owner)
+ where T : struct, ICEObject
+ {
+ if (owner is null || owner.IsDisposed)
+ {
+ return TargetReleaseOutcome.NotInvoked();
+ }
+
+ try
+ {
+ using LuaFrame frame = new(state);
+ LuaStatus status = owner.TryDestroy(state);
+ return status.IsOk
+ ? TargetReleaseOutcome.Released()
+ : TargetReleaseOutcome.Unconfirmed(EngineFailureKind.ProtectedLuaFailure);
+ }
+ catch (Exception)
+ {
+ return TargetReleaseOutcome.Unconfirmed(EngineFailureKind.BindingFailure);
+ }
+ finally
+ {
+ ConsumeOwner(owner);
+ }
+ }
+
+ private static void ConsumeOwner(Owned? owner)
+ where T : struct, ICEObject
+ {
+ if (owner is not null && !owner.IsDisposed)
+ {
+ try
+ {
+ _ = owner.Abandon();
+ }
+ catch (Exception)
+ {
+ // The session must not make IDisposable cleanup throw or expose a retry path after taking ownership.
+ }
}
}
@@ -739,10 +943,10 @@ private static void ValidateFirstRequest(in FirstScanRequest request)
// A default-initialized request can contain null strings. These are properties of the
// value-type request rather than parameters of this helper, therefore its parameter name
// must be the actual public argument ("request") instead of a local alias.
- ArgumentNullException.ThrowIfNull(request.Input1, nameof(request));
- ArgumentNullException.ThrowIfNull(request.Input2, nameof(request));
- ArgumentNullException.ThrowIfNull(request.ProtectionFlags, nameof(request));
- ArgumentNullException.ThrowIfNull(request.AlignmentParameter, nameof(request));
+ RequireValue(request.Input1, nameof(request));
+ RequireValue(request.Input2, nameof(request));
+ RequireValue(request.ProtectionFlags, nameof(request));
+ RequireValue(request.AlignmentParameter, nameof(request));
if (request.ScanOption is < ScanOption.UnknownValue or > ScanOption.SmallerThan)
{
@@ -771,8 +975,8 @@ private static void ValidateFirstRequest(in FirstScanRequest request)
private static void ValidateNextRequest(in NextScanRequest request)
{
- ArgumentNullException.ThrowIfNull(request.Input1, nameof(request));
- ArgumentNullException.ThrowIfNull(request.Input2, nameof(request));
+ RequireValue(request.Input1, nameof(request));
+ RequireValue(request.Input2, nameof(request));
if (request.ScanOption is < ScanOption.ExactValue or > ScanOption.Unchanged)
{
throw new ArgumentException("A next scan only accepts ExactValue through Unchanged, never UnknownValue.",
@@ -785,6 +989,14 @@ private static void ValidateNextRequest(in NextScanRequest request)
}
}
+ private static void RequireValue(string? value, string parameterName)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(parameterName);
+ }
+ }
+
private void ThrowIfCancelledBeforeNativeCall(CancellationToken cancellationToken)
{
LastCancellationMilestone = MemoryScanCancellationMilestone.None;
@@ -806,7 +1018,7 @@ private void ObserveCancellationAfterNativeCall(CancellationToken cancellationTo
private void EnsureCurrentContext(LuaState state, string operation)
{
- MemoryScanMaterializationStatus status = TryEnsureCurrentContext(state, operation);
+ MemoryScanMaterializationStatus status = TryEnsureCurrentContext(state);
if (status == MemoryScanMaterializationStatus.Success)
{
return;
@@ -823,7 +1035,7 @@ private void EnsureCurrentContext(LuaState state, string operation)
"'.");
}
- private MemoryScanMaterializationStatus TryEnsureCurrentContext(LuaState state, string operation)
+ private MemoryScanMaterializationStatus TryEnsureCurrentContext(LuaState state)
{
if (!_isBound || RuntimeIdentity != LuaRuntime.CurrentStateIdentity)
{
diff --git a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs
index 5421963b..3502fd93 100644
--- a/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs
+++ b/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs
@@ -4,6 +4,7 @@
using CheatEngine.SDK.Annotations.Lifetime;
using CheatEngine.SDK.Annotations.Threading;
using CheatEngine.SDK.Engine.Objects;
+using CheatEngine.SDK.Engine.Targets;
using CheatEngine.SDK.Lua.CompilerServices;
using CheatEngine.SDK.Lua.References;
using CheatEngine.SDK.Lua.Runtime;
@@ -71,7 +72,33 @@ public static bool TryCreate([NotNullWhen(true)] out MemoryScanSession? session)
[RequiresPluginEnabled]
public static MemoryScanCreationStatus TryCreateDetailed([NotNullWhen(true)] out MemoryScanSession? session)
{
- return TryCreateDetailedCore(out session, CreateSession);
+ return TryCreateWithOutcome(out session).Status;
+ }
+
+ ///
+ /// Creates a scanner/found-list pair and returns the pre-acquisition target observation with the factual factory
+ /// result.
+ ///
+ ///
+ /// The new context-bound session only when is
+ /// .
+ ///
+ ///
+ /// The factory result and the exact target observation made before createMemScan(). An unqualified
+ /// observation returns and invokes neither CE
+ /// factory.
+ ///
+ ///
+ /// The plugin is not enabled, the caller has no host Lua state, the host cannot push objects, or the caller is
+ /// not on Cheat Engine's main thread.
+ ///
+ [MainThreadOnly]
+ [RequiresPluginEnabled]
+ public static MemoryScanCreationOutcome TryCreateWithOutcome([NotNullWhen(true)] out MemoryScanSession? session)
+ {
+ MemoryScanCreationStatus status = TryCreateDetailedCore(out session, CreateSession,
+ out TargetSelectionObservation targetObservation);
+ return new MemoryScanCreationOutcome(status, targetObservation);
}
// Tests use this seam to prove that an ownership-transfer failure rolls the child back before its parent. The raw
@@ -81,11 +108,11 @@ public static MemoryScanCreationStatus TryCreateDetailed([NotNullWhen(true)] out
internal static bool TryCreateCore([NotNullWhen(true)] out MemoryScanSession? session,
MemoryScanSessionAdopter adopter)
{
- return TryCreateDetailedCore(out session, adopter) == MemoryScanCreationStatus.Success;
+ return TryCreateDetailedCore(out session, adopter, out _) == MemoryScanCreationStatus.Success;
}
private static MemoryScanCreationStatus TryCreateDetailedCore([NotNullWhen(true)] out MemoryScanSession? session,
- MemoryScanSessionAdopter adopter)
+ MemoryScanSessionAdopter adopter, out TargetSelectionObservation targetObservation)
{
ArgumentNullException.ThrowIfNull(adopter);
RequireEnabledMainThread();
@@ -99,9 +126,17 @@ private static MemoryScanCreationStatus TryCreateDetailedCore([NotNullWhen(true)
CEObject foundListHandle = CEObject.Null;
MemoryScanCreationStatus status = MemoryScanCreationStatus.Success;
MemoryScanSessionContext context = MemoryScanSessionContext.Capture(state);
+ targetObservation = context.TargetObservation;
session = null;
try
{
+ // A MemScan/FoundList pair acts against CE's ambient target. Do not acquire either caller-owned resource
+ // until the observation can identify a process incarnation for every later session operation and cleanup.
+ if (!context.TargetObservation.IsQualified)
+ {
+ return MemoryScanCreationStatus.TargetIdentityUnavailable;
+ }
+
status = TryCreateScanner(state, out scanner, out scannerHandle);
if (status == MemoryScanCreationStatus.Success)
{
@@ -119,12 +154,13 @@ private static MemoryScanCreationStatus TryCreateDetailedCore([NotNullWhen(true)
// Adoption transfers and empties both wrappers. Every other exit after construction must release the child
// before the parent while the original operation is still admitted. Swallowing a protected destroy failure
// avoids hiding the factory failure and, like Owned.Dispose, never retries an unknown native state.
- if (!TryRollback(state, foundList, foundListHandle) | !TryRollback(state, scanner, scannerHandle))
+ bool foundListRollbackFailed = !TryRollback(state, foundList, foundListHandle);
+ bool scannerRollbackFailed = !TryRollback(state, scanner, scannerHandle);
+ if ((foundListRollbackFailed || scannerRollbackFailed)
+ && session is null
+ && status != MemoryScanCreationStatus.Success)
{
- if (session is null && status != MemoryScanCreationStatus.Success)
- {
- status = MemoryScanCreationStatus.RollbackUnconfirmed;
- }
+ status = MemoryScanCreationStatus.RollbackUnconfirmed;
}
}
diff --git a/libs/CheatEngine.SDK.Lua/Callbacks/LuaCallback.cs b/libs/CheatEngine.SDK.Lua/Callbacks/LuaCallback.cs
index 5f9217e3..5a2f4670 100644
--- a/libs/CheatEngine.SDK.Lua/Callbacks/LuaCallback.cs
+++ b/libs/CheatEngine.SDK.Lua/Callbacks/LuaCallback.cs
@@ -34,7 +34,7 @@ namespace CheatEngine.SDK.Lua.Callbacks;
/// Inside the thunk: reads the state back from the upvalue.
///
///
-/// Release (, , or for
+/// Release (, , or for
/// whatever is
/// still alive): the closure's upvalue is set to a null light userdata first, so that a script which kept the
/// function and calls it later gets a "callback released" error instead of touching freed memory; only then is the
@@ -56,11 +56,11 @@ public abstract class LuaCallback : IDisposable
{
// Deterministic publication-race seam used only by the SDK's friend test assembly. Callback creation is cold, so
// its volatile read is deliberately kept out of callback invocation hot paths.
- internal static Action? BeforeRegistryAddForTesting;
+ private static Action? s_beforeRegistryAddForTesting;
// Deterministic disposal-race seam used only by the SDK's friend test assembly. It runs after an atomic admission
// refusal is observed, outside the runtime gate, so tests can let a failed transition reopen admission first.
- internal static Action? DisposeAdmissionRefusedForTesting;
+ private static Action? s_disposeAdmissionRefusedForTesting;
private readonly LuaRef _closure;
private readonly LuaStateIdentity _identity;
@@ -125,6 +125,28 @@ internal bool IsLinked
///
public void Dispose()
{
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ internal static void SetBeforeRegistryAddForTesting(Action? callback)
+ {
+ Volatile.Write(ref s_beforeRegistryAddForTesting, callback);
+ }
+
+ internal static void SetDisposeAdmissionRefusedForTesting(Action? callback)
+ {
+ Volatile.Write(ref s_disposeAdmissionRefusedForTesting, callback);
+ }
+
+ /// Releases the callback's managed and Lua resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
LuaRuntime.LuaCallbackDisposeOperationResult result =
LuaRuntime.TryAcquireOperationForCallbackDispose(out LuaRuntimeOperation operation);
if (result == LuaRuntime.LuaCallbackDisposeOperationResult.Acquired)
@@ -134,7 +156,6 @@ public void Dispose()
Release(operation.State);
}
- GC.SuppressFinalize(this);
return;
}
@@ -142,13 +163,11 @@ public void Dispose()
{
// This result was observed atomically with the closed gate. A later failed Detach can reopen admission,
// but cannot make it safe to abandon the closure before a transition-owned state neutralizes its upvalue.
- Volatile.Read(ref DisposeAdmissionRefusedForTesting)?.Invoke();
- GC.SuppressFinalize(this);
+ Volatile.Read(ref s_disposeAdmissionRefusedForTesting)?.Invoke();
return;
}
Release(default);
- GC.SuppressFinalize(this);
}
///
@@ -185,7 +204,7 @@ public static LuaStatus TryCreate(LuaState state, LuaNativeFunction thun
}
private static unsafe LuaStatus TryCreateCore(LuaState state, LuaNativeFunction thunk, TState stateObject,
- out LuaCallback? callback)
+ out LuaCallback? callback) // NOSONAR: this method is the required Lua C-ABI boundary.
where TState : class
{
callback = null;
@@ -221,7 +240,7 @@ private static unsafe LuaStatus TryCreateCore(LuaState state, LuaNativeF
}
LuaCallback created = new(handle, closure!, wrapped!);
- Volatile.Read(ref BeforeRegistryAddForTesting)?.Invoke();
+ Volatile.Read(ref s_beforeRegistryAddForTesting)?.Invoke();
LuaCallbackRegistry.Add(created);
callback = created;
transferred = true;
@@ -241,11 +260,9 @@ private static unsafe LuaStatus TryCreateCore(LuaState state, LuaNativeF
}
}
- private static unsafe LuaStatus TryCreateWrappedRef(
- LuaState state,
- lua_State* l,
- int top,
- out LuaRef? wrapped)
+ private static unsafe LuaStatus
+ TryCreateWrappedRef(LuaState state, lua_State* l, int top,
+ out LuaRef? wrapped) // NOSONAR: native Lua stack access is required here.
{
wrapped = null;
LuaStatus status = LuaHelpers.Push(l, LuaHelper.Wrap);
@@ -319,7 +336,7 @@ public void Release(LuaState state)
// Private so that Release, which holds the gate, is the only way in: the flag test, GCHandle.Dispose (not thread
// safe) and the unlink all depend on it.
- private unsafe void ReleaseUnderGate(LuaState state)
+ private unsafe void ReleaseUnderGate(LuaState state) // NOSONAR: native Lua stack access is required here.
{
if (_released)
{
@@ -360,12 +377,10 @@ private unsafe void ReleaseUnderGate(LuaState state)
}
}
- // This is the only unmanaged entry point for stateful LuaCallback instances. The user thunk remains a cdecl
- // function pointer stored in upvalue 2, while upvalue 1 deliberately retains the historical GCHandle
- // contract consumed by LuaThunk.TryGetState. A callback that begins before Detach closes admission keeps a lease;
- // a callback that begins after that boundary reports an ordinary Lua error and never enters plugin code.
+ // This is the only native entry point for stateful callbacks. The thunk address and managed state handle are kept in
+ // Lua upvalues, so callbacks already admitted before shutdown can finish while later calls fail safely.
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static unsafe int Dispatch(lua_State* pointer)
+ private static unsafe int Dispatch(lua_State* pointer) // NOSONAR: unmanaged callback entry point required by Lua.
{
LuaState state = new(pointer);
try
diff --git a/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs b/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs
index dd2dad8c..0f5119bf 100644
--- a/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs
+++ b/libs/CheatEngine.SDK.Lua/Runtime/LuaRuntime.cs
@@ -200,7 +200,7 @@ public static bool TryAcquireOperation(out LuaRuntimeOperation operation)
}
///
- /// Acquires an operation for and reports whether an attached lifecycle
+ /// Acquires an operation for and reports whether an attached lifecycle
/// transition, rather than an unavailable state, rejected it.
///
///
diff --git a/libs/CheatEngine.SDK.Lua/State/LuaState.Callbacks.cs b/libs/CheatEngine.SDK.Lua/State/LuaState.Callbacks.cs
index a56c0550..ffba013f 100644
--- a/libs/CheatEngine.SDK.Lua/State/LuaState.Callbacks.cs
+++ b/libs/CheatEngine.SDK.Lua/State/LuaState.Callbacks.cs
@@ -16,7 +16,8 @@
namespace CheatEngine.SDK.Lua.State;
// Pushing managed functions: with the error channel (the normal form) or without it.
-public readonly unsafe partial struct LuaState
+public readonly unsafe partial struct
+ LuaState // NOSONAR: this partial contains the required Lua C-ABI function pointers.
{
///
/// Pushes a generated [LuaFunction] closure whose invocation is bound to one host attachment and Lua
diff --git a/native/cheatengine-sdk-lua-bridge/cheatengine_sdk_lua_bridge.c b/native/cheatengine-sdk-lua-bridge/cheatengine_sdk_lua_bridge.c
index 6781ab5b..06b878a5 100644
--- a/native/cheatengine-sdk-lua-bridge/cheatengine_sdk_lua_bridge.c
+++ b/native/cheatengine-sdk-lua-bridge/cheatengine_sdk_lua_bridge.c
@@ -18,35 +18,38 @@
/* This file deliberately does not include lua.h. The host owns Lua; callers provide
* addresses resolved from that host module, so the bridge can never load a second Lua. */
typedef struct lua_State lua_State;
-typedef int (CHEATENGINE_SDK_CALL *lua_CFunction)(lua_State *);
+typedef int (CHEATENGINE_SDK_CALL *lua_CFunction)(lua_State*);
typedef int64_t lua_Integer;
typedef intptr_t lua_KContext;
-typedef int (CHEATENGINE_SDK_CALL *lua_KFunction)(lua_State *, int, lua_KContext);
-typedef void (CHEATENGINE_SDK_HOST_CALL *cheatengine_sdk_host_object_pusher)(lua_State *, void *);
-typedef struct cheatengine_sdk_lua_exports {
- int (CHEATENGINE_SDK_CALL *gettop)(lua_State *);
- void (CHEATENGINE_SDK_CALL *settop)(lua_State *, int);
- int (CHEATENGINE_SDK_CALL *checkstack)(lua_State *, int);
- void (CHEATENGINE_SDK_CALL *rotate)(lua_State *, int, int);
- const char *(CHEATENGINE_SDK_CALL *pushlstring)(lua_State *, const char *, size_t);
- void (CHEATENGINE_SDK_CALL *pushinteger)(lua_State *, lua_Integer);
- void (CHEATENGINE_SDK_CALL *createtable)(lua_State *, int, int);
- void *(CHEATENGINE_SDK_CALL *newuserdata)(lua_State *, size_t);
- void (CHEATENGINE_SDK_CALL *pushcclosure)(lua_State *, lua_CFunction, int);
- void (CHEATENGINE_SDK_CALL *pushlightuserdata)(lua_State *, void *);
- void (CHEATENGINE_SDK_CALL *rawset)(lua_State *, int);
- void (CHEATENGINE_SDK_CALL *rawseti)(lua_State *, int, lua_Integer);
- void (CHEATENGINE_SDK_CALL *rawsetp)(lua_State *, int, const void *);
- int (CHEATENGINE_SDK_CALL *rawgetp)(lua_State *, int, const void *);
- int (CHEATENGINE_SDK_CALL *rawgeti)(lua_State *, int, lua_Integer);
- int (CHEATENGINE_SDK_CALL *type)(lua_State *, int);
- int (CHEATENGINE_SDK_CALL *pcallk)(lua_State *, int, int, int, lua_KContext, lua_KFunction);
- int (CHEATENGINE_SDK_CALL *error)(lua_State *);
- int (CHEATENGINE_SDK_CALL *l_ref)(lua_State *, int);
- void (CHEATENGINE_SDK_CALL *l_unref)(lua_State *, int, int);
+typedef int (CHEATENGINE_SDK_CALL *lua_KFunction)(lua_State*, int, lua_KContext);
+typedef void (CHEATENGINE_SDK_HOST_CALL *cheatengine_sdk_host_object_pusher)(lua_State*, void*);
+
+typedef struct cheatengine_sdk_lua_exports
+{
+ int (CHEATENGINE_SDK_CALL *gettop)(lua_State*);
+ void (CHEATENGINE_SDK_CALL *settop)(lua_State*, int);
+ int (CHEATENGINE_SDK_CALL *checkstack)(lua_State*, int);
+ void (CHEATENGINE_SDK_CALL *rotate)(lua_State*, int, int);
+ const char*(CHEATENGINE_SDK_CALL *pushlstring)(lua_State*, const char*, size_t);
+ void (CHEATENGINE_SDK_CALL *pushinteger)(lua_State*, lua_Integer);
+ void (CHEATENGINE_SDK_CALL *createtable)(lua_State*, int, int);
+ void*(CHEATENGINE_SDK_CALL *newuserdata)(lua_State*, size_t);
+ void (CHEATENGINE_SDK_CALL *pushcclosure)(lua_State*, lua_CFunction, int);
+ void (CHEATENGINE_SDK_CALL *pushlightuserdata)(lua_State*, void*);
+ void (CHEATENGINE_SDK_CALL *rawset)(lua_State*, int);
+ void (CHEATENGINE_SDK_CALL *rawseti)(lua_State*, int, lua_Integer);
+ void (CHEATENGINE_SDK_CALL *rawsetp)(lua_State*, int, const void*);
+ int (CHEATENGINE_SDK_CALL *rawgetp)(lua_State*, int, const void*);
+ int (CHEATENGINE_SDK_CALL *rawgeti)(lua_State*, int, lua_Integer);
+ int (CHEATENGINE_SDK_CALL *type)(lua_State*, int);
+ int (CHEATENGINE_SDK_CALL *pcallk)(lua_State*, int, int, int, lua_KContext, lua_KFunction);
+ int (CHEATENGINE_SDK_CALL *error)(lua_State*);
+ int (CHEATENGINE_SDK_CALL *l_ref)(lua_State*, int);
+ void (CHEATENGINE_SDK_CALL *l_unref)(lua_State*, int, int);
} cheatengine_sdk_lua_exports;
-typedef struct cheatengine_sdk_lua_bridge_contract {
+typedef struct cheatengine_sdk_lua_bridge_contract
+{
uint32_t magic;
uint32_t contract_size;
uint64_t supported_operations;
@@ -61,7 +64,9 @@ typedef struct cheatengine_sdk_lua_bridge_contract {
} cheatengine_sdk_lua_bridge_contract;
enum { LUA_OK = 0, LUA_MULTRET = -1, LUA_TTABLE = 5, LUA_REGISTRYINDEX = -1001000, CHEATENGINE_SDK_NO_ERROR = -100 };
-enum {
+
+enum
+{
OP_PUSH_BYTES = 0,
OP_CREATE_TABLE = 1,
OP_NEW_USERDATA = 2,
@@ -76,8 +81,15 @@ enum {
OP_PUSH_BYTE_TABLE = 11,
CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT = 12
};
-enum { CHEATENGINE_SDK_LUA_BRIDGE_LEGACY_ABI_VERSION = 1, CHEATENGINE_SDK_LUA_BRIDGE_ABI_MAJOR = 1, CHEATENGINE_SDK_LUA_BRIDGE_ABI_MINOR = 1 };
+
+enum
+{
+ CHEATENGINE_SDK_LUA_BRIDGE_LEGACY_ABI_VERSION = 1, CHEATENGINE_SDK_LUA_BRIDGE_ABI_MAJOR = 1,
+ CHEATENGINE_SDK_LUA_BRIDGE_ABI_MINOR = 1
+};
+
enum { CHEATENGINE_SDK_LUA_BRIDGE_MAGIC = 0x4345534B };
+
#define CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_MASK \
((UINT64_C(1) << OP_PUSH_BYTES) | (UINT64_C(1) << OP_CREATE_TABLE) | \
(UINT64_C(1) << OP_NEW_USERDATA) | (UINT64_C(1) << OP_PUSH_CLOSURE) | \
@@ -88,66 +100,101 @@ enum { CHEATENGINE_SDK_LUA_BRIDGE_MAGIC = 0x4345534B };
_Static_assert(CHAR_BIT == 8, "The Lua bridge requires eight-bit bytes.");
_Static_assert(sizeof(int) == 4, "The Lua bridge ABI requires a 32-bit int.");
-_Static_assert(sizeof(void *) == 8, "The Lua bridge supports Windows x64 only.");
-_Static_assert(sizeof(size_t) == sizeof(void *), "size_t must be pointer-sized.");
+_Static_assert(sizeof(void*) == 8, "The Lua bridge supports Windows x64 only.");
+_Static_assert(sizeof(size_t) == sizeof(void*), "size_t must be pointer-sized.");
_Static_assert(sizeof(lua_Integer) == 8, "The supported Lua build uses a 64-bit lua_Integer.");
-_Static_assert(sizeof(lua_KContext) == sizeof(void *), "lua_KContext must be pointer-sized.");
+_Static_assert(sizeof(lua_KContext) == sizeof(void*), "lua_KContext must be pointer-sized.");
_Static_assert(CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT <= 64, "The operation bitmap has no remaining bit.");
-_Static_assert(sizeof(cheatengine_sdk_lua_exports) == 20 * sizeof(void *), "The Lua export table layout changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, gettop) == 0 * sizeof(void *), "lua_gettop has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, settop) == 1 * sizeof(void *), "lua_settop has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, checkstack) == 2 * sizeof(void *), "lua_checkstack has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, rotate) == 3 * sizeof(void *), "lua_rotate has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushlstring) == 4 * sizeof(void *), "lua_pushlstring has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushinteger) == 5 * sizeof(void *), "lua_pushinteger has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, createtable) == 6 * sizeof(void *), "lua_createtable has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, newuserdata) == 7 * sizeof(void *), "lua_newuserdata has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushcclosure) == 8 * sizeof(void *), "lua_pushcclosure has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushlightuserdata) == 9 * sizeof(void *), "lua_pushlightuserdata has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawset) == 10 * sizeof(void *), "lua_rawset has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawseti) == 11 * sizeof(void *), "lua_rawseti has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawsetp) == 12 * sizeof(void *), "lua_rawsetp has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawgetp) == 13 * sizeof(void *), "lua_rawgetp has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawgeti) == 14 * sizeof(void *), "lua_rawgeti has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, type) == 15 * sizeof(void *), "lua_type has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, pcallk) == 16 * sizeof(void *), "lua_pcallk has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, error) == 17 * sizeof(void *), "lua_error has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, l_ref) == 18 * sizeof(void *), "luaL_ref has an unexpected export-table offset.");
-_Static_assert(offsetof(cheatengine_sdk_lua_exports, l_unref) == 19 * sizeof(void *), "luaL_unref has an unexpected export-table offset.");
+_Static_assert(sizeof(cheatengine_sdk_lua_exports) == 20 * sizeof(void*), "The Lua export table layout changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, gettop) == 0 * sizeof(void*),
+ "lua_gettop has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, settop) == 1 * sizeof(void*),
+ "lua_settop has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, checkstack) == 2 * sizeof(void*),
+ "lua_checkstack has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, rotate) == 3 * sizeof(void*),
+ "lua_rotate has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushlstring) == 4 * sizeof(void*),
+ "lua_pushlstring has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushinteger) == 5 * sizeof(void*),
+ "lua_pushinteger has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, createtable) == 6 * sizeof(void*),
+ "lua_createtable has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, newuserdata) == 7 * sizeof(void*),
+ "lua_newuserdata has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushcclosure) == 8 * sizeof(void*),
+ "lua_pushcclosure has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, pushlightuserdata) == 9 * sizeof(void*),
+ "lua_pushlightuserdata has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawset) == 10 * sizeof(void*),
+ "lua_rawset has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawseti) == 11 * sizeof(void*),
+ "lua_rawseti has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawsetp) == 12 * sizeof(void*),
+ "lua_rawsetp has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawgetp) == 13 * sizeof(void*),
+ "lua_rawgetp has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, rawgeti) == 14 * sizeof(void*),
+ "lua_rawgeti has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, type) == 15 * sizeof(void*),
+ "lua_type has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, pcallk) == 16 * sizeof(void*),
+ "lua_pcallk has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, error) == 17 * sizeof(void*),
+ "lua_error has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, l_ref) == 18 * sizeof(void*),
+ "luaL_ref has an unexpected export-table offset.");
+_Static_assert(offsetof(cheatengine_sdk_lua_exports, l_unref) == 19 * sizeof(void*),
+ "luaL_unref has an unexpected export-table offset.");
_Static_assert(sizeof(cheatengine_sdk_lua_bridge_contract) == 32, "The bridge contract layout changed.");
_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, magic) == 0, "The bridge contract magic offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, contract_size) == 4, "The bridge contract size offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, supported_operations) == 8, "The bridge contract operation bitmap offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, export_table_size) == 16, "The bridge contract table-size offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, abi_major) == 20, "The bridge contract major-version offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, abi_minor) == 22, "The bridge contract minor-version offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, pointer_size) == 24, "The bridge contract pointer-size offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, lua_integer_size) == 25, "The bridge contract lua_Integer-size offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, size_t_size) == 26, "The bridge contract size_t-size offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, reserved) == 27, "The bridge contract reserved offset changed.");
-_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, reserved_padding) == 28, "The bridge contract padding offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, contract_size) == 4,
+ "The bridge contract size offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, supported_operations) == 8,
+ "The bridge contract operation bitmap offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, export_table_size) == 16,
+ "The bridge contract table-size offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, abi_major) == 20,
+ "The bridge contract major-version offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, abi_minor) == 22,
+ "The bridge contract minor-version offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, pointer_size) == 24,
+ "The bridge contract pointer-size offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, lua_integer_size) == 25,
+ "The bridge contract lua_Integer-size offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, size_t_size) == 26,
+ "The bridge contract size_t-size offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, reserved) == 27,
+ "The bridge contract reserved offset changed.");
+_Static_assert(offsetof(cheatengine_sdk_lua_bridge_contract, reserved_padding) == 28,
+ "The bridge contract padding offset changed.");
-typedef struct call_context {
- const cheatengine_sdk_lua_exports *api;
+typedef struct call_context
+{
+ const cheatengine_sdk_lua_exports* api;
int op;
int input_count;
- const void *data;
+ const void* data;
size_t size;
intptr_t a;
intptr_t b;
} call_context;
-static CHEATENGINE_SDK_THREAD_LOCAL call_context *s_context;
-CHEATENGINE_SDK_EXPORT const char cheatengine_sdk_lua_bridge_source_fingerprint[] = CHEATENGINE_SDK_LUA_BRIDGE_SOURCE_FINGERPRINT;
+static CHEATENGINE_SDK_THREAD_LOCAL call_context* s_context;
-CHEATENGINE_SDK_EXPORT uint32_t CHEATENGINE_SDK_CALL cheatengine_sdk_lua_bridge_abi_version(void) {
+CHEATENGINE_SDK_EXPORT const char cheatengine_sdk_lua_bridge_source_fingerprint[] =
+ CHEATENGINE_SDK_LUA_BRIDGE_SOURCE_FINGERPRINT;
+
+CHEATENGINE_SDK_EXPORT uint32_t CHEATENGINE_SDK_CALL cheatengine_sdk_lua_bridge_abi_version(void)
+{
/* Retained for already-published managed clients. New clients consume the full contract below. */
return CHEATENGINE_SDK_LUA_BRIDGE_LEGACY_ABI_VERSION;
}
CHEATENGINE_SDK_EXPORT int CHEATENGINE_SDK_CALL cheatengine_sdk_lua_bridge_get_contract(
- cheatengine_sdk_lua_bridge_contract *contract,
- size_t contract_size) {
+ cheatengine_sdk_lua_bridge_contract* contract,
+ size_t contract_size)
+{
const cheatengine_sdk_lua_bridge_contract value = {
CHEATENGINE_SDK_LUA_BRIDGE_MAGIC,
(uint32_t)sizeof(cheatengine_sdk_lua_bridge_contract),
@@ -155,11 +202,11 @@ CHEATENGINE_SDK_EXPORT int CHEATENGINE_SDK_CALL cheatengine_sdk_lua_bridge_get_c
(uint32_t)sizeof(cheatengine_sdk_lua_exports),
CHEATENGINE_SDK_LUA_BRIDGE_ABI_MAJOR,
CHEATENGINE_SDK_LUA_BRIDGE_ABI_MINOR,
- (uint8_t)sizeof(void *),
+ (uint8_t)sizeof(void*),
(uint8_t)sizeof(lua_Integer),
(uint8_t)sizeof(size_t),
0,
- { 0 }
+ {0}
};
if (!contract || contract_size != sizeof(value)) return 0;
@@ -167,13 +214,16 @@ CHEATENGINE_SDK_EXPORT int CHEATENGINE_SDK_CALL cheatengine_sdk_lua_bridge_get_c
return 1;
}
-static int fail(lua_State *L, const char *message) {
- size_t length = 0; while (message[length]) ++length;
+static int fail(lua_State* L, const char* message)
+{
+ size_t length = 0;
+ while (message[length]) ++length;
s_context->api->pushlstring(L, message, length);
return s_context->api->error(L);
}
-static int lua_exports_are_complete(const cheatengine_sdk_lua_exports *api) {
+static int lua_exports_are_complete(const cheatengine_sdk_lua_exports* api)
+{
return api && api->gettop && api->settop && api->checkstack && api->rotate &&
api->pushlstring && api->pushinteger && api->createtable && api->newuserdata &&
api->pushcclosure && api->pushlightuserdata && api->rawset && api->rawseti &&
@@ -181,40 +231,47 @@ static int lua_exports_are_complete(const cheatengine_sdk_lua_exports *api) {
api->error && api->l_ref && api->l_unref;
}
-static int is_nonnegative_int(intptr_t value) {
+static int is_nonnegative_int(intptr_t value)
+{
return value >= 0 && value <= INT_MAX;
}
-static int is_int(intptr_t value) {
+static int is_int(intptr_t value)
+{
return value >= INT_MIN && value <= INT_MAX;
}
-static int CHEATENGINE_SDK_CALL operation(lua_State *L) {
- call_context *c = s_context;
- const cheatengine_sdk_lua_exports *a = c->api;
+static int CHEATENGINE_SDK_CALL operation(lua_State* L)
+{
+ call_context* c = s_context;
+ const cheatengine_sdk_lua_exports* a = c->api;
- switch (c->op) {
- case OP_PUSH_BYTES: {
- static const char empty = 0;
- if (c->input_count != 0 || (c->size != 0 && !c->data))
- return fail(L, "CheatEngine.SDK protected byte push has invalid arguments");
- a->pushlstring(L, c->size ? (const char *)c->data : &empty, c->size);
- return 1;
- }
- case OP_PUSH_BYTE_TABLE: {
- const uint8_t *bytes = (const uint8_t *)c->data;
- size_t index;
- if (c->input_count != 0 || c->size > INT_MAX || (c->size != 0 && !bytes))
- return fail(L, "CheatEngine.SDK protected byte-table push has invalid arguments");
- if (!a->checkstack(L, 2))
- return fail(L, "CheatEngine.SDK protected byte-table push could not reserve stack slots");
- a->createtable(L, (int)c->size, 0);
- for (index = 0; index < c->size; ++index) {
- a->pushinteger(L, (lua_Integer)bytes[index]);
- a->rawseti(L, -2, (lua_Integer)(index + 1));
+ switch (c->op)
+ {
+ case OP_PUSH_BYTES:
+ {
+ static const char empty = 0;
+ if (c->input_count != 0 || (c->size != 0 && !c->data))
+ return fail(L, "CheatEngine.SDK protected byte push has invalid arguments");
+ a->pushlstring(L, c->size ? (const char*)c->data : &empty, c->size);
+ return 1;
+ }
+ case OP_PUSH_BYTE_TABLE:
+ {
+ const uint8_t* bytes = c->data;
+ size_t index;
+ if (c->input_count != 0 || c->size > INT_MAX || (c->size != 0 && !bytes))
+ return fail(L, "CheatEngine.SDK protected byte-table push has invalid arguments");
+ if (!a->checkstack(L, 2))
+ return fail(L, "CheatEngine.SDK protected byte-table push could not reserve stack slots");
+ a->createtable(L, (int)c->size, 0);
+ for (index = 0; index < c->size; ++index)
+ {
+ a->pushinteger(L, (lua_Integer)bytes[index]);
+ a->rawseti(L, -2, (lua_Integer)(index + 1));
+ }
+ return 1;
}
- return 1;
- }
case OP_CREATE_TABLE:
if (c->input_count != 0 || !is_nonnegative_int(c->a) || !is_nonnegative_int(c->b))
return fail(L, "CheatEngine.SDK protected table creation has invalid capacities");
@@ -246,16 +303,25 @@ static int CHEATENGINE_SDK_CALL operation(lua_State *L) {
return fail(L, "CheatEngine.SDK protected pointer raw set has invalid inputs");
a->rawsetp(L, 1, c->data);
return 0;
- case OP_REF: {
- /* Argument one is the value. Get/create an SDK-private registry table keyed by stableKey. */
- if (c->input_count != 1)
- return fail(L, "CheatEngine.SDK protected reference creation has invalid inputs");
- a->rawgetp(L, LUA_REGISTRYINDEX, c->data);
- if (a->type(L, -1) != LUA_TTABLE) { a->settop(L, -2); a->createtable(L, 0, 0); a->pushlightuserdata(L, (void *)c->data); a->rotate(L, -2, 1); a->rawset(L, LUA_REGISTRYINDEX); a->rawgetp(L, LUA_REGISTRYINDEX, c->data); }
- a->rotate(L, 1, 1); /* table, value */
- a->pushinteger(L, (lua_Integer)a->l_ref(L, 1));
- return 1;
- }
+ case OP_REF:
+ {
+ /* Argument one is the value. Get/create an SDK-private registry table keyed by stableKey. */
+ if (c->input_count != 1)
+ return fail(L, "CheatEngine.SDK protected reference creation has invalid inputs");
+ a->rawgetp(L, LUA_REGISTRYINDEX, c->data);
+ if (a->type(L, -1) != LUA_TTABLE)
+ {
+ a->settop(L, -2);
+ a->createtable(L, 0, 0);
+ a->pushlightuserdata(L, (void*)c->data);
+ a->rotate(L, -2, 1);
+ a->rawset(L, LUA_REGISTRYINDEX);
+ a->rawgetp(L, LUA_REGISTRYINDEX, c->data);
+ }
+ a->rotate(L, 1, 1); /* table, value */
+ a->pushinteger(L, (lua_Integer)a->l_ref(L, 1));
+ return 1;
+ }
case OP_PUSH_REF:
if (c->input_count != 0)
return fail(L, "CheatEngine.SDK protected reference push has invalid inputs");
@@ -275,7 +341,7 @@ static int CHEATENGINE_SDK_CALL operation(lua_State *L) {
case OP_PUSH_HOST_OBJECT:
if (c->input_count != 0 || !c->data)
return fail(L, "CheatEngine.SDK host-object pusher is unavailable");
- ((cheatengine_sdk_host_object_pusher)c->data)(L, (void *)c->a);
+ ((cheatengine_sdk_host_object_pusher)c->data)(L, (void*)c->a);
return 1;
default:
return fail(L, "CheatEngine.SDK protected Lua operation is invalid");
@@ -284,10 +350,13 @@ static int CHEATENGINE_SDK_CALL operation(lua_State *L) {
/* Inputs already sit at top. A light C function needs no allocation, then pcall moves all
* allocating/raising work below Lua's protected boundary. */
-CHEATENGINE_SDK_EXPORT int CHEATENGINE_SDK_CALL cheatengine_sdk_lua_protected(lua_State *L, const cheatengine_sdk_lua_exports *api, int op, int input_count, const void *data, size_t size, intptr_t a, intptr_t b) {
+CHEATENGINE_SDK_EXPORT int CHEATENGINE_SDK_CALL cheatengine_sdk_lua_protected(
+ lua_State* L, const cheatengine_sdk_lua_exports* api, int op, int input_count, const void* data, size_t size,
+ intptr_t a, intptr_t b)
+{
int top;
call_context context;
- call_context *previous;
+ call_context* previous;
int status;
if (!L || !lua_exports_are_complete(api) || input_count < 0 || input_count == INT_MAX)
diff --git a/native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll b/native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll
index d160e846..11808ce6 100644
Binary files a/native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll and b/native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll differ
diff --git a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/Shapes/PluginShape.cs b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/Shapes/PluginShape.cs
index dbd0ab2a..98d6924b 100644
--- a/source-generators/CheatEngine.SDK.SourceGenerators.Shared/Shapes/PluginShape.cs
+++ b/source-generators/CheatEngine.SDK.SourceGenerators.Shared/Shapes/PluginShape.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Microsoft.CodeAnalysis;
@@ -327,12 +328,10 @@ private static bool HasRequiredMembers(INamedTypeSymbol type)
{
for (INamedTypeSymbol? current = type; current is not null; current = current.BaseType)
{
- foreach (ISymbol member in current.GetMembers())
+ if (current.GetMembers().Any(static member =>
+ member is IPropertySymbol { IsRequired: true } or IFieldSymbol { IsRequired: true }))
{
- if (member is IPropertySymbol { IsRequired: true } or IFieldSymbol { IsRequired: true })
- {
- return true;
- }
+ return true;
}
}
@@ -347,16 +346,9 @@ private static bool IsObsoleteError(ISymbol symbol, INamedTypeSymbol? obsoleteAt
return false;
}
- foreach (AttributeData attribute in symbol.GetAttributes())
- {
- if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, obsoleteAttribute)
- && attribute.ConstructorArguments is [_, { Value: true } _])
- {
- return true;
- }
- }
-
- return false;
+ return symbol.GetAttributes().Any(attribute =>
+ SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, obsoleteAttribute)
+ && attribute.ConstructorArguments is [_, { Value: true } _]);
}
private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol? attributeClass)
@@ -366,15 +358,8 @@ private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol? attributeClas
return false;
}
- foreach (AttributeData attribute in symbol.GetAttributes())
- {
- if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeClass))
- {
- return true;
- }
- }
-
- return false;
+ return symbol.GetAttributes().Any(attribute =>
+ SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeClass));
}
// The generated factory is a top-level type of the same assembly with no inheritance relation to the plugin:
diff --git a/tests/CheatEngine.SDK.Abi.Tests/Fixture/NativeAbiFixtureManagedComparisonTests.cs b/tests/CheatEngine.SDK.Abi.Tests/Fixture/NativeAbiFixtureManagedComparisonTests.cs
index f2e9769f..509142ec 100644
--- a/tests/CheatEngine.SDK.Abi.Tests/Fixture/NativeAbiFixtureManagedComparisonTests.cs
+++ b/tests/CheatEngine.SDK.Abi.Tests/Fixture/NativeAbiFixtureManagedComparisonTests.cs
@@ -1,5 +1,5 @@
-using System.Collections.Generic;
using System.Globalization;
+
using CheatEngine.SDK.Abi.Native;
using CheatEngine.SDK.Abi.Tests.Support;
@@ -12,237 +12,251 @@ namespace CheatEngine.SDK.Abi.Tests.Fixture;
///
public sealed unsafe class NativeAbiFixtureManagedComparisonTests
{
- /// Name of the CI-provided absolute path to the validated native fixture facts file.
- internal const string FactsPathEnvironmentVariable = "CE77_NATIVE_ABI_FACTS_PATH";
-
- /// Name of the opt-in gate that makes the native fixture facts mandatory.
- internal const string RequiredEnvironmentVariable = "CE77_NATIVE_ABI_REQUIRED";
-
- [Fact]
- public void Native_fixture_layout_facts_match_the_managed_x64_measurements_when_CI_supplies_them()
- {
- var factsPath = ResolveFactsPath(
- Environment.GetEnvironmentVariable(FactsPathEnvironmentVariable),
- Environment.GetEnvironmentVariable(RequiredEnvironmentVariable));
- if (factsPath is null)
- {
- Assert.Null(factsPath);
- return;
- }
-
- Assert.True(Layout.Is64BitProcess, Layout.Requires64BitProcess);
-
- var nativeFacts = ReadFacts(factsPath);
- var managedFacts = CreateManagedLayoutFacts();
- foreach (var managedFact in managedFacts)
- {
- Assert.True(nativeFacts.TryGetValue(managedFact.Key, out var nativeValue),
- $"The native ABI fixture omitted '{managedFact.Key}'.");
- Assert.Equal(managedFact.Value, nativeValue);
- }
- }
-
- [Fact]
- public void Native_fixture_facts_path_is_optional_when_required_mode_is_not_enabled()
- {
- Assert.Null(ResolveFactsPath(null, null));
- }
-
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- [InlineData(" \t")]
- public void Native_fixture_required_mode_rejects_an_absent_facts_path(string? factsPath)
- {
- var exception = Assert.Throws(() => ResolveFactsPath(factsPath, "true"));
-
- Assert.Equal(
- $"'{RequiredEnvironmentVariable}=true' requires '{FactsPathEnvironmentVariable}' to name a validated native ABI fixture facts file.",
- exception.Message);
- }
-
- [Fact]
- public void Native_fixture_comparison_rejects_a_supplied_missing_facts_file()
- {
- var factsPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.txt");
-
- var exception = Assert.Throws(() => ReadFacts(factsPath));
-
- Assert.Equal(factsPath, exception.FileName);
- }
-
- private static string? ResolveFactsPath(string? factsPath, string? requiredMode)
- {
- if (!string.IsNullOrWhiteSpace(factsPath)) return factsPath;
-
- if (string.Equals(requiredMode, "true", StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException(
- $"'{RequiredEnvironmentVariable}=true' requires '{FactsPathEnvironmentVariable}' to name a validated native ABI fixture facts file.");
- }
-
- return null;
- }
-
- private static Dictionary ReadFacts(string factsPath)
- {
- if (!File.Exists(factsPath))
- {
- throw new FileNotFoundException($"The native ABI fixture facts file '{factsPath}' was not found.", factsPath);
- }
-
- var facts = new Dictionary(StringComparer.Ordinal);
- foreach (var line in File.ReadLines(factsPath))
- {
- if (string.IsNullOrWhiteSpace(line)) continue;
-
- var separator = line.IndexOf('=');
- Assert.True(separator > 0, $"The native ABI fixture fact '{line}' is not key=value.");
- var key = line[..separator];
- var value = line[(separator + 1)..];
- Assert.True(facts.TryAdd(key, value), $"The native ABI fixture emitted duplicate fact '{key}'.");
- }
-
- return facts;
- }
-
- private static Dictionary CreateManagedLayoutFacts()
- {
- var facts = new Dictionary(StringComparer.Ordinal);
- AddLayout(facts, "plugin_version");
- AddLayout(facts, "plugin_type0_record");
- AddLayout(facts, "plugin_type0_init");
- AddLayout(facts, "plugin_type1_init");
- AddLayout(facts, "plugin_type2_init");
- AddLayout(facts, "plugin_type3_init");
- AddLayout(facts, "plugin_type4_init");
- AddLayout(facts, "plugin_type5_init");
- AddLayout(facts, "plugin_type6_init");
- AddLayout(facts, "plugin_type7_init");
- AddLayout(facts, "plugin_type8_init");
- AddLayout(facts, "register_modification_info");
- AddLayout(facts, "exported_functions_prefix");
-
- PluginVersion pluginVersion = default;
- void* pluginVersionOrigin = &pluginVersion;
- AddOffset(facts, "plugin_version.Version", Layout.OffsetOf(pluginVersionOrigin, &pluginVersion.Version));
- AddOffset(facts, "plugin_version.PluginName", Layout.OffsetOf(pluginVersionOrigin, &pluginVersion.PluginName));
-
- PluginType0Record pluginType0Record = default;
- void* pluginType0RecordOrigin = &pluginType0Record;
- AddOffset(facts, "plugin_type0_record.InterpretedAddress",
- Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.InterpretedAddress));
- AddOffset(facts, "plugin_type0_record.Address", Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Address));
- AddOffset(facts, "plugin_type0_record.IsPointer", Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.IsPointer));
- AddOffset(facts, "plugin_type0_record.CountOffsets",
- Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.CountOffsets));
- AddOffset(facts, "plugin_type0_record.Offsets", Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Offsets));
- AddOffset(facts, "plugin_type0_record.Description",
- Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Description));
- AddOffset(facts, "plugin_type0_record.ValueType", Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.ValueType));
- AddOffset(facts, "plugin_type0_record.Size", Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Size));
-
- AddPluginInitOffsets(facts);
- AddRegisterModificationOffsets(facts);
- AddExportedFunctionsPrefixOffsets(facts);
- return facts;
- }
-
- private static void AddPluginInitOffsets(Dictionary facts)
- {
- AddressListPluginInit type0 = default;
- void* type0Origin = &type0;
- AddOffset(facts, "plugin_type0_init.Name", Layout.OffsetOf(type0Origin, &type0.Name));
- AddOffset(facts, "plugin_type0_init.Callback", Layout.OffsetOf(type0Origin, &type0.Callback));
-
- MemoryViewPluginInit type1 = default;
- void* type1Origin = &type1;
- AddOffset(facts, "plugin_type1_init.Name", Layout.OffsetOf(type1Origin, &type1.Name));
- AddOffset(facts, "plugin_type1_init.Callback", Layout.OffsetOf(type1Origin, &type1.Callback));
- AddOffset(facts, "plugin_type1_init.Shortcut", Layout.OffsetOf(type1Origin, &type1.Shortcut));
-
- DebugEventPluginInit type2 = default;
- AddOffset(facts, "plugin_type2_init.Callback", Layout.OffsetOf(&type2, &type2.Callback));
-
- ProcessWatcherPluginInit type3 = default;
- AddOffset(facts, "plugin_type3_init.Callback", Layout.OffsetOf(&type3, &type3.Callback));
-
- FunctionPointerChangePluginInit type4 = default;
- AddOffset(facts, "plugin_type4_init.Callback", Layout.OffsetOf(&type4, &type4.Callback));
-
- MainMenuPluginInit type5 = default;
- void* type5Origin = &type5;
- AddOffset(facts, "plugin_type5_init.Name", Layout.OffsetOf(type5Origin, &type5.Name));
- AddOffset(facts, "plugin_type5_init.Callback", Layout.OffsetOf(type5Origin, &type5.Callback));
- AddOffset(facts, "plugin_type5_init.Shortcut", Layout.OffsetOf(type5Origin, &type5.Shortcut));
-
- DisassemblerContextPluginInit type6 = default;
- void* type6Origin = &type6;
- AddOffset(facts, "plugin_type6_init.Name", Layout.OffsetOf(type6Origin, &type6.Name));
- AddOffset(facts, "plugin_type6_init.Callback", Layout.OffsetOf(type6Origin, &type6.Callback));
- AddOffset(facts, "plugin_type6_init.CallbackOnPopup", Layout.OffsetOf(type6Origin, &type6.CallbackOnPopup));
- AddOffset(facts, "plugin_type6_init.Shortcut", Layout.OffsetOf(type6Origin, &type6.Shortcut));
-
- DisassemblerRenderLinePluginInit type7 = default;
- AddOffset(facts, "plugin_type7_init.Callback", Layout.OffsetOf(&type7, &type7.Callback));
-
- AutoAssemblerPluginInit type8 = default;
- AddOffset(facts, "plugin_type8_init.Callback", Layout.OffsetOf(&type8, &type8.Callback));
- }
-
- private static void AddRegisterModificationOffsets(Dictionary facts)
- {
- RegisterModificationInfo info = default;
- void* origin = &info;
- AddOffset(facts, "register_modification_info.Address", Layout.OffsetOf(origin, &info.Address));
- AddOffset(facts, "register_modification_info.ChangeEax", Layout.OffsetOf(origin, &info.ChangeEax));
- AddOffset(facts, "register_modification_info.ChangeR15", Layout.OffsetOf(origin, &info.ChangeR15));
- AddOffset(facts, "register_modification_info.ChangeOf", Layout.OffsetOf(origin, &info.ChangeOf));
- AddOffset(facts, "register_modification_info.NewEax", Layout.OffsetOf(origin, &info.NewEax));
- AddOffset(facts, "register_modification_info.NewR15", Layout.OffsetOf(origin, &info.NewR15));
- AddOffset(facts, "register_modification_info.NewCf", Layout.OffsetOf(origin, &info.NewCf));
- AddOffset(facts, "register_modification_info.NewOf", Layout.OffsetOf(origin, &info.NewOf));
- }
-
- private static void AddExportedFunctionsPrefixOffsets(Dictionary facts)
- {
- ExportedFunctionsPrefix exports = default;
- void* origin = &exports;
- AddOffset(facts, "exported_functions_prefix.SizeOfExportedFunctions",
- Layout.OffsetOf(origin, &exports.SizeOfExportedFunctions));
- AddOffset(facts, "exported_functions_prefix.ShowMessage", Layout.OffsetOf(origin, &exports.ShowMessage));
- AddOffset(facts, "exported_functions_prefix.RegisterFunction", Layout.OffsetOf(origin, &exports.RegisterFunction));
- AddOffset(facts, "exported_functions_prefix.UnregisterFunction",
- Layout.OffsetOf(origin, &exports.UnregisterFunction));
- AddOffset(facts, "exported_functions_prefix.OpenedProcessId", Layout.OffsetOf(origin, &exports.OpenedProcessId));
- AddOffset(facts, "exported_functions_prefix.OpenedProcessHandle",
- Layout.OffsetOf(origin, &exports.OpenedProcessHandle));
- AddOffset(facts, "exported_functions_prefix.GetMainWindowHandle",
- Layout.OffsetOf(origin, &exports.GetMainWindowHandle));
- AddOffset(facts, "exported_functions_prefix.AutoAssemble", Layout.OffsetOf(origin, &exports.AutoAssemble));
- AddOffset(facts, "exported_functions_prefix.Assembler", Layout.OffsetOf(origin, &exports.Assembler));
- AddOffset(facts, "exported_functions_prefix.Disassembler", Layout.OffsetOf(origin, &exports.Disassembler));
- AddOffset(facts, "exported_functions_prefix.ChangeRegistersAtAddress",
- Layout.OffsetOf(origin, &exports.ChangeRegistersAtAddress));
- AddOffset(facts, "exported_functions_prefix.InjectDll", Layout.OffsetOf(origin, &exports.InjectDll));
- AddOffset(facts, "exported_functions_prefix.FreezeMemory", Layout.OffsetOf(origin, &exports.FreezeMemory));
- AddOffset(facts, "exported_functions_prefix.UnfreezeMemory", Layout.OffsetOf(origin, &exports.UnfreezeMemory));
- AddOffset(facts, "exported_functions_prefix.FixMemory", Layout.OffsetOf(origin, &exports.FixMemory));
- AddOffset(facts, "exported_functions_prefix.ProcessList", Layout.OffsetOf(origin, &exports.ProcessList));
- AddOffset(facts, "exported_functions_prefix.ReloadSettings", Layout.OffsetOf(origin, &exports.ReloadSettings));
- AddOffset(facts, "exported_functions_prefix.GetAddressFromPointer",
- Layout.OffsetOf(origin, &exports.GetAddressFromPointer));
- }
-
- private static void AddLayout(Dictionary facts, string key)
- where T : unmanaged
- {
- facts.Add($"sizeof.{key}", Layout.SizeOf().ToString(CultureInfo.InvariantCulture));
- facts.Add($"alignof.{key}", Layout.AlignmentOf().ToString(CultureInfo.InvariantCulture));
- }
-
- private static void AddOffset(Dictionary facts, string key, int value)
- {
- facts.Add($"offsetof.{key}", value.ToString(CultureInfo.InvariantCulture));
- }
+ /// Name of the CI-provided absolute path to the validated native fixture facts file.
+ internal const string FactsPathEnvironmentVariable = "CE77_NATIVE_ABI_FACTS_PATH";
+
+ /// Name of the opt-in gate that makes the native fixture facts mandatory.
+ internal const string RequiredEnvironmentVariable = "CE77_NATIVE_ABI_REQUIRED";
+
+ [Fact]
+ public void Native_fixture_layout_facts_match_the_managed_x64_measurements_when_CI_supplies_them()
+ {
+ string? factsPath = ResolveFactsPath(
+ Environment.GetEnvironmentVariable(FactsPathEnvironmentVariable),
+ Environment.GetEnvironmentVariable(RequiredEnvironmentVariable));
+ if (factsPath is null)
+ {
+ Assert.Null(factsPath);
+ return;
+ }
+
+ Assert.True(Layout.Is64BitProcess, Layout.Requires64BitProcess);
+
+ Dictionary nativeFacts = ReadFacts(factsPath);
+ Dictionary managedFacts = CreateManagedLayoutFacts();
+ foreach (KeyValuePair managedFact in managedFacts)
+ {
+ Assert.True(nativeFacts.TryGetValue(managedFact.Key, out string? nativeValue),
+ $"The native ABI fixture omitted '{managedFact.Key}'.");
+ Assert.Equal(managedFact.Value, nativeValue);
+ }
+ }
+
+ [Fact]
+ public void Native_fixture_facts_path_is_optional_when_required_mode_is_not_enabled()
+ {
+ Assert.Null(ResolveFactsPath(null, null));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" \t")]
+ public void Native_fixture_required_mode_rejects_an_absent_facts_path(string? factsPath)
+ {
+ InvalidOperationException exception =
+ Assert.Throws(() => ResolveFactsPath(factsPath, "true"));
+
+ Assert.Equal(
+ $"'{RequiredEnvironmentVariable}=true' requires '{FactsPathEnvironmentVariable}' to name a validated native ABI fixture facts file.",
+ exception.Message);
+ }
+
+ [Fact]
+ public void Native_fixture_comparison_rejects_a_supplied_missing_facts_file()
+ {
+ string factsPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.txt");
+
+ FileNotFoundException exception = Assert.Throws(() => ReadFacts(factsPath));
+
+ Assert.Equal(factsPath, exception.FileName);
+ }
+
+ private static string? ResolveFactsPath(string? factsPath, string? requiredMode)
+ {
+ if (!string.IsNullOrWhiteSpace(factsPath))
+ {
+ return factsPath;
+ }
+
+ if (string.Equals(requiredMode, "true", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(
+ $"'{RequiredEnvironmentVariable}=true' requires '{FactsPathEnvironmentVariable}' to name a validated native ABI fixture facts file.");
+ }
+
+ return null;
+ }
+
+ private static Dictionary ReadFacts(string factsPath)
+ {
+ if (!File.Exists(factsPath))
+ {
+ throw new FileNotFoundException($"The native ABI fixture facts file '{factsPath}' was not found.",
+ factsPath);
+ }
+
+ Dictionary facts = new(StringComparer.Ordinal);
+ foreach (string line in File.ReadLines(factsPath))
+ {
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ int separator = line.IndexOf('=');
+ Assert.True(separator > 0, $"The native ABI fixture fact '{line}' is not key=value.");
+ string key = line[..separator];
+ string value = line[(separator + 1)..];
+ Assert.True(facts.TryAdd(key, value), $"The native ABI fixture emitted duplicate fact '{key}'.");
+ }
+
+ return facts;
+ }
+
+ private static Dictionary CreateManagedLayoutFacts()
+ {
+ Dictionary facts = new(StringComparer.Ordinal);
+ AddLayout(facts, "plugin_version");
+ AddLayout(facts, "plugin_type0_record");
+ AddLayout(facts, "plugin_type0_init");
+ AddLayout(facts, "plugin_type1_init");
+ AddLayout(facts, "plugin_type2_init");
+ AddLayout(facts, "plugin_type3_init");
+ AddLayout(facts, "plugin_type4_init");
+ AddLayout(facts, "plugin_type5_init");
+ AddLayout(facts, "plugin_type6_init");
+ AddLayout(facts, "plugin_type7_init");
+ AddLayout(facts, "plugin_type8_init");
+ AddLayout(facts, "register_modification_info");
+ AddLayout(facts, "exported_functions_prefix");
+
+ PluginVersion pluginVersion = default;
+ void* pluginVersionOrigin = &pluginVersion;
+ AddOffset(facts, "plugin_version.Version", Layout.OffsetOf(pluginVersionOrigin, &pluginVersion.Version));
+ AddOffset(facts, "plugin_version.PluginName", Layout.OffsetOf(pluginVersionOrigin, &pluginVersion.PluginName));
+
+ PluginType0Record pluginType0Record = default;
+ void* pluginType0RecordOrigin = &pluginType0Record;
+ AddOffset(facts, "plugin_type0_record.InterpretedAddress",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.InterpretedAddress));
+ AddOffset(facts, "plugin_type0_record.Address",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Address));
+ AddOffset(facts, "plugin_type0_record.IsPointer",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.IsPointer));
+ AddOffset(facts, "plugin_type0_record.CountOffsets",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.CountOffsets));
+ AddOffset(facts, "plugin_type0_record.Offsets",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Offsets));
+ AddOffset(facts, "plugin_type0_record.Description",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Description));
+ AddOffset(facts, "plugin_type0_record.ValueType",
+ Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.ValueType));
+ AddOffset(facts, "plugin_type0_record.Size", Layout.OffsetOf(pluginType0RecordOrigin, &pluginType0Record.Size));
+
+ AddPluginInitOffsets(facts);
+ AddRegisterModificationOffsets(facts);
+ AddExportedFunctionsPrefixOffsets(facts);
+ return facts;
+ }
+
+ private static void AddPluginInitOffsets(Dictionary facts)
+ {
+ AddressListPluginInit type0 = default;
+ void* type0Origin = &type0;
+ AddOffset(facts, "plugin_type0_init.Name", Layout.OffsetOf(type0Origin, &type0.Name));
+ AddOffset(facts, "plugin_type0_init.Callback", Layout.OffsetOf(type0Origin, &type0.Callback));
+
+ MemoryViewPluginInit type1 = default;
+ void* type1Origin = &type1;
+ AddOffset(facts, "plugin_type1_init.Name", Layout.OffsetOf(type1Origin, &type1.Name));
+ AddOffset(facts, "plugin_type1_init.Callback", Layout.OffsetOf(type1Origin, &type1.Callback));
+ AddOffset(facts, "plugin_type1_init.Shortcut", Layout.OffsetOf(type1Origin, &type1.Shortcut));
+
+ DebugEventPluginInit type2 = default;
+ AddOffset(facts, "plugin_type2_init.Callback", Layout.OffsetOf(&type2, &type2.Callback));
+
+ ProcessWatcherPluginInit type3 = default;
+ AddOffset(facts, "plugin_type3_init.Callback", Layout.OffsetOf(&type3, &type3.Callback));
+
+ FunctionPointerChangePluginInit type4 = default;
+ AddOffset(facts, "plugin_type4_init.Callback", Layout.OffsetOf(&type4, &type4.Callback));
+
+ MainMenuPluginInit type5 = default;
+ void* type5Origin = &type5;
+ AddOffset(facts, "plugin_type5_init.Name", Layout.OffsetOf(type5Origin, &type5.Name));
+ AddOffset(facts, "plugin_type5_init.Callback", Layout.OffsetOf(type5Origin, &type5.Callback));
+ AddOffset(facts, "plugin_type5_init.Shortcut", Layout.OffsetOf(type5Origin, &type5.Shortcut));
+
+ DisassemblerContextPluginInit type6 = default;
+ void* type6Origin = &type6;
+ AddOffset(facts, "plugin_type6_init.Name", Layout.OffsetOf(type6Origin, &type6.Name));
+ AddOffset(facts, "plugin_type6_init.Callback", Layout.OffsetOf(type6Origin, &type6.Callback));
+ AddOffset(facts, "plugin_type6_init.CallbackOnPopup", Layout.OffsetOf(type6Origin, &type6.CallbackOnPopup));
+ AddOffset(facts, "plugin_type6_init.Shortcut", Layout.OffsetOf(type6Origin, &type6.Shortcut));
+
+ DisassemblerRenderLinePluginInit type7 = default;
+ AddOffset(facts, "plugin_type7_init.Callback", Layout.OffsetOf(&type7, &type7.Callback));
+
+ AutoAssemblerPluginInit type8 = default;
+ AddOffset(facts, "plugin_type8_init.Callback", Layout.OffsetOf(&type8, &type8.Callback));
+ }
+
+ private static void AddRegisterModificationOffsets(Dictionary facts)
+ {
+ RegisterModificationInfo info = default;
+ void* origin = &info;
+ AddOffset(facts, "register_modification_info.Address", Layout.OffsetOf(origin, &info.Address));
+ AddOffset(facts, "register_modification_info.ChangeEax", Layout.OffsetOf(origin, &info.ChangeEax));
+ AddOffset(facts, "register_modification_info.ChangeR15", Layout.OffsetOf(origin, &info.ChangeR15));
+ AddOffset(facts, "register_modification_info.ChangeOf", Layout.OffsetOf(origin, &info.ChangeOf));
+ AddOffset(facts, "register_modification_info.NewEax", Layout.OffsetOf(origin, &info.NewEax));
+ AddOffset(facts, "register_modification_info.NewR15", Layout.OffsetOf(origin, &info.NewR15));
+ AddOffset(facts, "register_modification_info.NewCf", Layout.OffsetOf(origin, &info.NewCf));
+ AddOffset(facts, "register_modification_info.NewOf", Layout.OffsetOf(origin, &info.NewOf));
+ }
+
+ private static void AddExportedFunctionsPrefixOffsets(Dictionary facts)
+ {
+ ExportedFunctionsPrefix exports = default;
+ void* origin = &exports;
+ AddOffset(facts, "exported_functions_prefix.SizeOfExportedFunctions",
+ Layout.OffsetOf(origin, &exports.SizeOfExportedFunctions));
+ AddOffset(facts, "exported_functions_prefix.ShowMessage", Layout.OffsetOf(origin, &exports.ShowMessage));
+ AddOffset(facts, "exported_functions_prefix.RegisterFunction",
+ Layout.OffsetOf(origin, &exports.RegisterFunction));
+ AddOffset(facts, "exported_functions_prefix.UnregisterFunction",
+ Layout.OffsetOf(origin, &exports.UnregisterFunction));
+ AddOffset(facts, "exported_functions_prefix.OpenedProcessId",
+ Layout.OffsetOf(origin, &exports.OpenedProcessId));
+ AddOffset(facts, "exported_functions_prefix.OpenedProcessHandle",
+ Layout.OffsetOf(origin, &exports.OpenedProcessHandle));
+ AddOffset(facts, "exported_functions_prefix.GetMainWindowHandle",
+ Layout.OffsetOf(origin, &exports.GetMainWindowHandle));
+ AddOffset(facts, "exported_functions_prefix.AutoAssemble", Layout.OffsetOf(origin, &exports.AutoAssemble));
+ AddOffset(facts, "exported_functions_prefix.Assembler", Layout.OffsetOf(origin, &exports.Assembler));
+ AddOffset(facts, "exported_functions_prefix.Disassembler", Layout.OffsetOf(origin, &exports.Disassembler));
+ AddOffset(facts, "exported_functions_prefix.ChangeRegistersAtAddress",
+ Layout.OffsetOf(origin, &exports.ChangeRegistersAtAddress));
+ AddOffset(facts, "exported_functions_prefix.InjectDll", Layout.OffsetOf(origin, &exports.InjectDll));
+ AddOffset(facts, "exported_functions_prefix.FreezeMemory", Layout.OffsetOf(origin, &exports.FreezeMemory));
+ AddOffset(facts, "exported_functions_prefix.UnfreezeMemory", Layout.OffsetOf(origin, &exports.UnfreezeMemory));
+ AddOffset(facts, "exported_functions_prefix.FixMemory", Layout.OffsetOf(origin, &exports.FixMemory));
+ AddOffset(facts, "exported_functions_prefix.ProcessList", Layout.OffsetOf(origin, &exports.ProcessList));
+ AddOffset(facts, "exported_functions_prefix.ReloadSettings", Layout.OffsetOf(origin, &exports.ReloadSettings));
+ AddOffset(facts, "exported_functions_prefix.GetAddressFromPointer",
+ Layout.OffsetOf(origin, &exports.GetAddressFromPointer));
+ }
+
+ private static void AddLayout(Dictionary facts, string key)
+ where T : unmanaged
+ {
+ facts.Add($"sizeof.{key}", Layout.SizeOf().ToString(CultureInfo.InvariantCulture));
+ facts.Add($"alignof.{key}", Layout.AlignmentOf().ToString(CultureInfo.InvariantCulture));
+ }
+
+ private static void AddOffset(Dictionary facts, string key, int value)
+ {
+ facts.Add($"offsetof.{key}", value.ToString(CultureInfo.InvariantCulture));
+ }
}
diff --git a/tests/CheatEngine.SDK.Analyzers.Tests/Architecture/LuaDirectApiBoundaryGuardTests.cs b/tests/CheatEngine.SDK.Analyzers.Tests/Architecture/LuaDirectApiBoundaryGuardTests.cs
index 5bee2180..59e14150 100644
--- a/tests/CheatEngine.SDK.Analyzers.Tests/Architecture/LuaDirectApiBoundaryGuardTests.cs
+++ b/tests/CheatEngine.SDK.Analyzers.Tests/Architecture/LuaDirectApiBoundaryGuardTests.cs
@@ -185,6 +185,60 @@ void PushUncheckedFunction(lua_CFunction thunk)
Assert.Empty(violations);
}
+ [Fact]
+ public void Guard_allows_the_audited_fast_path_when_the_throw_guard_uses_a_block()
+ {
+ LuaDirectApiPolicy policy = LoadPolicy();
+ List violations = InspectSource(policy, """
+ using static CheatEngine.SDK.Lua.Interop.Api.LuaApi;
+
+ unsafe struct C
+ {
+ private lua_State* Pointer;
+
+ void PushUncheckedFunction(lua_CFunction thunk)
+ {
+ if (lua_checkstack(Pointer, 1) == 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ lua_pushcclosure(Pointer, thunk, 0);
+ }
+ }
+ """, LightCFunctionFastPathSourcePath);
+
+ Assert.Empty(violations);
+ }
+
+ [Fact]
+ public void Guard_rejects_the_audited_fast_path_when_a_braced_throw_guard_contains_a_Lua_call()
+ {
+ LuaDirectApiPolicy policy = LoadPolicy();
+ List violations = InspectSource(policy, """
+ using static CheatEngine.SDK.Lua.Interop.Api.LuaApi;
+
+ unsafe struct C
+ {
+ private lua_State* Pointer;
+
+ void PushUncheckedFunction(lua_CFunction thunk)
+ {
+ if (lua_checkstack(Pointer, 1) == 0)
+ {
+ lua_pushinteger(Pointer, 42);
+ throw new InvalidOperationException();
+ }
+
+ lua_pushcclosure(Pointer, thunk, 0);
+ }
+ }
+ """, LightCFunctionFastPathSourcePath);
+
+ GuardViolation violation = Assert.Single(violations);
+ Assert.Equal("lua_pushcclosure", violation.MemberName);
+ }
+
[Fact]
public void Guard_reports_the_audited_path_when_a_Lua_call_interrupts_the_reservation_and_push()
{
diff --git a/tests/CheatEngine.SDK.Benchmarks/CallbackBenchmarks.cs b/tests/CheatEngine.SDK.Benchmarks/CallbackBenchmarks.cs
index b8d47b6f..b5d0e195 100644
--- a/tests/CheatEngine.SDK.Benchmarks/CallbackBenchmarks.cs
+++ b/tests/CheatEngine.SDK.Benchmarks/CallbackBenchmarks.cs
@@ -16,7 +16,7 @@ namespace CheatEngine.SDK.Benchmarks;
///
[MemoryDiagnoser(false)]
[BenchmarkCategory("Callbacks")]
-public class CallbackBenchmarks : IDisposable
+public sealed class CallbackBenchmarks : IDisposable
{
/// Lua-side calls per invocation of .
private const int LoopCount = 1000;
@@ -28,10 +28,20 @@ public class CallbackBenchmarks : IDisposable
///
public void Dispose()
{
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
_ = BenchFunctions.UnregisterLuaFunctions(_l);
LuaRuntime.Detach();
_state?.Dispose();
- GC.SuppressFinalize(this);
}
/// Opens a state, registers the thunk and defines the Lua-side loop that calls it times.
@@ -42,7 +52,7 @@ public void Setup()
_state = new NativeLuaState();
_l = FakeHostRuntime.Attach(_state, false);
- var registered = BenchFunctions.RegisterLuaFunctions(_l);
+ LuaStatus registered = BenchFunctions.RegisterLuaFunctions(_l);
if (!registered.IsOk)
{
throw new InvalidOperationException("RegisterLuaFunctions failed: " + LuaError.FromStack(_l, registered));
@@ -60,7 +70,7 @@ public void Setup()
///
/// Unregisters the thunk, detaches the ambient runtime and closes the state. BenchmarkDotNet does not call
- /// itself; this is what [GlobalCleanup] is for.
+ /// itself; this is what [GlobalCleanup] is for.
///
[GlobalCleanup]
public void Cleanup()
diff --git a/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs
index e30df03c..8e618dc8 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/Assembly/InstructionOperationsTests.cs
@@ -1,8 +1,10 @@
-using System;
+using System.Text;
+
using CheatEngine.SDK.Engine.Assembly;
using CheatEngine.SDK.Engine.Runtime;
using CheatEngine.SDK.Engine.Tests.Support;
using CheatEngine.SDK.Engine.Values;
+using CheatEngine.SDK.Lua.State;
using CheatEngine.SDK.Tests.Shared.NativeLua;
namespace CheatEngine.SDK.Engine.Tests.Assembly;
@@ -14,354 +16,362 @@ namespace CheatEngine.SDK.Engine.Tests.Assembly;
[Trait("Category", "NativeLua")]
public sealed class InstructionOperationsTests
{
- [Fact]
- public void ObserveCurrent_builds_an_x64_profile_from_coherent_target_probes_and_rejects_a_contradiction()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
-
- InstructionOperationStatus observed = InstructionProfiles.TryObserveCurrent(out var targetProfile);
-
- Assert.Equal(InstructionOperationStatus.Success, observed);
- Assert.Equal(4242, targetProfile.Target.Value);
- Assert.Equal(CheatEngineArchitecture.X64, targetProfile.Profile.Architecture);
- Assert.Equal(PointerSize.Bit64, targetProfile.Profile.AddressWidth);
-
- EngineTest.Run(scope.State, "instruction_target_is_x86 = true\ninstruction_target_is_64bit = true"u8);
- InstructionOperationStatus contradictory = InstructionProfiles.TryObserveCurrent(out var rejectedProfile);
-
- Assert.Equal(InstructionOperationStatus.InvalidProfile, contradictory);
- Assert.Equal(default, rejectedProfile);
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void ObserveCurrent_uses_the_target_ARM_probe_and_matching_width_without_using_the_host_width()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
-
- EngineTest.Run(scope.State, "instruction_target_is_64bit = false\ninstruction_target_is_arm = true"u8);
- InstructionTargetProfile arm32 = Observe(scope.State);
-
- EngineTest.Run(scope.State, "instruction_target_is_64bit = true"u8);
- InstructionTargetProfile arm64 = Observe(scope.State);
-
- Assert.Equal(CheatEngineArchitecture.Arm32, arm32.Profile.Architecture);
- Assert.Equal(PointerSize.Bit32, arm32.Profile.AddressWidth);
- Assert.Equal(CheatEngineArchitecture.Arm64, arm64.Profile.Architecture);
- Assert.Equal(PointerSize.Bit64, arm64.Profile.AddressWidth);
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Assemble_passes_the_explicit_relative_origin_and_copies_the_complete_byte_table()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile targetProfile = Observe(scope.State);
- Span destination = stackalloc byte[5];
- Address origin = 0x0000_0001_4000_1000UL;
-
- InstructionOperationStatus status = InstructionAssembler.TryAssemble(targetProfile, "jmp rel", origin,
- destination, out var written, out var requiredLength);
-
- Assert.Equal(InstructionOperationStatus.Success, status);
- Assert.Equal(5, written);
- Assert.Equal(5, requiredLength);
- Assert.Equal(new byte[] { 0xE9, 0xFB, 0xFF, 0xFF, 0xFF }, destination.ToArray());
- Assert.Equal(origin.ToInt64(), ReadInteger(scope.State, "instruction_assemble_origin"));
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Assemble_rejection_capacity_and_malformed_tables_do_not_publish_a_prefix()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile targetProfile = Observe(scope.State);
-
- Span rejectedDestination = stackalloc byte[5];
- rejectedDestination.Fill(0xA5);
- InstructionOperationStatus rejected = InstructionAssembler.TryAssemble(targetProfile, "reject", 0x140001000UL,
- rejectedDestination, out var rejectedWritten, out var rejectedRequired);
-
- Assert.Equal(InstructionOperationStatus.InstructionRejected, rejected);
- Assert.Equal(0, rejectedWritten);
- Assert.Equal(0, rejectedRequired);
- Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5, 0xA5 }, rejectedDestination.ToArray());
-
- Span tooSmallDestination = stackalloc byte[4];
- tooSmallDestination.Fill(0xA5);
- InstructionOperationStatus tooSmall = InstructionAssembler.TryAssemble(targetProfile, "jmp rel", 0x140001000UL,
- tooSmallDestination, out var tooSmallWritten, out var tooSmallRequired);
-
- Assert.Equal(InstructionOperationStatus.DestinationTooSmall, tooSmall);
- Assert.Equal(0, tooSmallWritten);
- Assert.Equal(5, tooSmallRequired);
- Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5 }, tooSmallDestination.ToArray());
-
- Span malformedDestination = stackalloc byte[2];
- malformedDestination.Fill(0xA5);
- InstructionOperationStatus malformed = InstructionAssembler.TryAssemble(targetProfile, "malformed", 0x140001000UL,
- malformedDestination, out var malformedWritten, out var malformedRequired);
-
- Assert.Equal(InstructionOperationStatus.InvalidResult, malformed);
- Assert.Equal(0, malformedWritten);
- Assert.Equal(0, malformedRequired);
- Assert.Equal(new byte[] { 0xA5, 0xA5 }, malformedDestination.ToArray());
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Assemble_rejects_an_address_wider_than_the_observed_x86_profile_before_entering_Lua()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- EngineTest.Run(scope.State, "instruction_target_is_64bit = false\ninstruction_target_is_x86 = true"u8);
- InstructionTargetProfile targetProfile = Observe(scope.State);
- Span destination = stackalloc byte[5];
-
- InstructionOperationStatus overwide = InstructionAssembler.TryAssemble(targetProfile, "jmp rel", 0x1_0000_0000UL,
- destination, out var written, out var requiredLength);
-
- Assert.Equal(InstructionOperationStatus.AddressExceedsProfileWidth, overwide);
- Assert.Equal(0, written);
- Assert.Equal(0, requiredLength);
- Assert.Equal(0, ReadInteger(scope.State, "instruction_assemble_calls"));
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Assemble_reports_a_target_change_without_copying_the_returned_bytes()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile targetProfile = Observe(scope.State);
- EngineTest.Run(scope.State, "instruction_assemble_behavior = \"target-change\""u8);
- Span destination = stackalloc byte[1];
- destination[0] = 0xA5;
-
- InstructionOperationStatus status = InstructionAssembler.TryAssemble(targetProfile, "nop", 0x140001000UL,
- destination, out var written, out var requiredLength);
-
- Assert.Equal(InstructionOperationStatus.TargetChanged, status);
- Assert.Equal(0, written);
- Assert.Equal(0, requiredLength);
- Assert.Equal(0xA5, destination[0]);
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Disassemble_parses_the_fixture_line_inside_the_SDK_and_copies_all_text()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile targetProfile = Observe(scope.State);
- Address address = 0x0000_0001_4000_1000UL;
-
- InstructionOperationStatus status = InstructionDisassembler.TryDisassemble(targetProfile, address, 256,
- out var instruction, out var requiredUtf8Bytes);
-
- Assert.Equal(InstructionOperationStatus.Success, status);
- Assert.Equal(address, instruction.Address);
- Assert.Equal("0000000140001000", instruction.AddressText);
- Assert.Equal("E9 FB FF FF FF", instruction.Bytes);
- Assert.Equal("jmp", instruction.Opcode);
- Assert.Equal("0000000140001000", instruction.Extra);
- Assert.Equal(49, instruction.Utf8ByteLength);
- Assert.Equal(55, requiredUtf8Bytes);
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Disassemble_enforces_the_raw_text_bound_and_rejects_malformed_split_results()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile targetProfile = Observe(scope.State);
-
- InstructionOperationStatus bounded = InstructionDisassembler.TryDisassemble(targetProfile, 0x140001000UL, 4,
- out var boundedInstruction, out var requiredUtf8Bytes);
- InstructionOperationStatus malformed = InstructionDisassembler.TryDisassemble(targetProfile, 0xBADUL, 256,
- out var malformedInstruction, out var malformedRequiredUtf8Bytes);
- InstructionOperationStatus oversizedFields = InstructionDisassembler.TryDisassemble(targetProfile, 0xBADDUL, 10,
- out var oversizedInstruction, out var oversizedRequiredUtf8Bytes);
-
- Assert.Equal(InstructionOperationStatus.OutputTooLong, bounded);
- Assert.Equal(default, boundedInstruction);
- Assert.Equal(55, requiredUtf8Bytes);
- Assert.Equal(InstructionOperationStatus.InvalidResult, malformed);
- Assert.Equal(default, malformedInstruction);
- Assert.Equal(9, malformedRequiredUtf8Bytes);
- Assert.Equal(InstructionOperationStatus.OutputTooLong, oversizedFields);
- Assert.Equal(default, oversizedInstruction);
- Assert.Equal(10, oversizedRequiredUtf8Bytes);
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Missing_and_raising_instruction_globals_have_distinct_structured_outcomes()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using (HostScope missingScope = new(state))
- {
- InstallProfileGlobals(missingScope.State);
- InstructionTargetProfile targetProfile = Observe(missingScope.State);
- InstructionOperationStatus unavailable = InstructionDisassembler.TryDisassemble(targetProfile, 0x140001000UL,
- 256, out _, out _);
-
- Assert.Equal(InstructionOperationStatus.GlobalUnavailable, unavailable);
- Assert.Equal(0, missingScope.State.Top);
- }
-
- using HostScope raisingScope = new(state);
- InstallInstructionGlobals(raisingScope.State);
- InstructionTargetProfile raisingProfile = Observe(raisingScope.State);
- InstructionOperationStatus failure = InstructionDisassembler.TryDisassemble(raisingProfile, 0xDEADUL,
- 256, out _, out _);
-
- Assert.Equal(InstructionOperationStatus.LuaFailure, failure);
- Assert.Equal(0, raisingScope.State.Top);
- }
-
- [Fact]
- public void Navigation_returns_a_positive_length_and_an_explicitly_estimated_previous_address()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile targetProfile = Observe(scope.State);
-
- InstructionOperationStatus lengthStatus = InstructionNavigator.TryGetLength(targetProfile, 0x140001000UL,
- out var length);
- InstructionOperationStatus previousStatus = InstructionNavigator.TryGetPrevious(targetProfile, 0x140001000UL,
- out var previous);
-
- Assert.Equal(InstructionOperationStatus.Success, lengthStatus);
- Assert.Equal(5, length);
- Assert.Equal(InstructionOperationStatus.Success, previousStatus);
- Assert.Equal(0x140000FFBUL, previous.Value);
- Assert.Equal(0, scope.State.Top);
- }
-
- [Fact]
- public void Navigation_rejects_nonpositive_or_string_lengths_and_addresses_wider_than_the_profile()
- {
- EngineTest.RequireNativeLua();
- using NativeLuaState state = new();
- using HostScope scope = new(state);
- InstallInstructionGlobals(scope.State);
- InstructionTargetProfile x64Profile = Observe(scope.State);
-
- InstructionOperationStatus invalidLength = InstructionNavigator.TryGetLength(x64Profile, 0UL, out var zeroLength);
- InstructionOperationStatus stringLength = InstructionNavigator.TryGetLength(x64Profile, 1UL, out var coercedLength);
-
- EngineTest.Run(scope.State, "instruction_target_is_64bit = false\ninstruction_target_is_x86 = true"u8);
- InstructionTargetProfile x86Profile = Observe(scope.State);
- InstructionOperationStatus overwidePrevious = InstructionNavigator.TryGetPrevious(x86Profile, 0x1000UL,
- out var previous);
-
- Assert.Equal(InstructionOperationStatus.InvalidResult, invalidLength);
- Assert.Equal(0, zeroLength);
- Assert.Equal(InstructionOperationStatus.InvalidResult, stringLength);
- Assert.Equal(0, coercedLength);
- Assert.Equal(InstructionOperationStatus.AddressExceedsProfileWidth, overwidePrevious);
- Assert.Equal(Address.Zero, previous);
- Assert.Equal(0, scope.State.Top);
- }
-
- private static InstructionTargetProfile Observe(CheatEngine.SDK.Lua.State.LuaState state)
- {
- InstructionOperationStatus status = InstructionProfiles.TryObserveCurrent(out var targetProfile);
- Assert.Equal(InstructionOperationStatus.Success, status);
- Assert.Equal(0, state.Top);
- return targetProfile;
- }
-
- private static void InstallInstructionGlobals(CheatEngine.SDK.Lua.State.LuaState state)
- {
- InstallProfileGlobals(state);
- EngineTest.Run(state, """
- instruction_assemble_calls = 0
- instruction_assemble_origin = 0
- instruction_assemble_behavior = "normal"
-
- assemble = function(line, address)
- instruction_assemble_calls = instruction_assemble_calls + 1
- instruction_assemble_origin = address
- if instruction_assemble_behavior == "target-change" then
- instruction_target_process_id = 7777
- return { 0x90 }, nil
- end
- if line == "reject" then return nil, "instruction rejected" end
- if line == "malformed" then return { 0x90, 300 }, nil end
- if line == "raise" then error("assembly failure") end
- return { 0xE9, 0xFB, 0xFF, 0xFF, 0xFF }, nil
- end
-
- disassemble = function(address)
- if address == 0xDEAD then error("disassembly failure") end
- if address == 0xBAD then return "bad-split" end
- if address == 0xBADD then return "wide-split" end
- return "0000000140001000 E9 FB FF FF FF jmp 0000000140001000"
- end
-
- splitDisassembledString = function(line)
- if line == "bad-split" then return "only-one" end
- if line == "wide-split" then return "aaaa", "bbbb", "cccc", "dddd" end
- return "0000000140001000", "E9 FB FF FF FF", "jmp", "0000000140001000"
- end
-
- getInstructionSize = function(address)
- if address == 0 then return 0 end
- if address == 1 then return "5" end
- return 5
- end
-
- getPreviousOpcode = function(address)
- if address == 0x1000 then return 0x100000000 end
- return address - 5
- end
- """u8);
- }
-
- private static void InstallProfileGlobals(CheatEngine.SDK.Lua.State.LuaState state)
- {
- EngineTest.Run(state, """
- instruction_target_process_id = 4242
- instruction_target_is_64bit = true
- instruction_target_is_x86 = false
- instruction_target_is_arm = false
- function getOpenedProcessID() return instruction_target_process_id end
- function targetIs64Bit() return instruction_target_is_64bit end
- function targetIsX86() return instruction_target_is_x86 end
- function targetIsArm() return instruction_target_is_arm end
- """u8);
- }
-
- private static long ReadInteger(CheatEngine.SDK.Lua.State.LuaState state, string name)
- {
- using var frame = new CheatEngine.SDK.Lua.State.LuaFrame(state);
- Assert.True(state.TryGetGlobal(System.Text.Encoding.UTF8.GetBytes(name)).IsOk);
- return EngineTest.ReadInteger(state, -1);
- }
+ [Fact]
+ public void ObserveCurrent_builds_an_x64_profile_from_coherent_target_probes_and_rejects_a_contradiction()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+
+ InstructionOperationStatus observed =
+ InstructionProfiles.TryObserveCurrent(out InstructionTargetProfile targetProfile);
+
+ Assert.Equal(InstructionOperationStatus.Success, observed);
+ Assert.Equal(4242, targetProfile.Target.Value);
+ Assert.Equal(CheatEngineArchitecture.X64, targetProfile.Profile.Architecture);
+ Assert.Equal(PointerSize.Bit64, targetProfile.Profile.AddressWidth);
+
+ EngineTest.Run(scope.State, "instruction_target_is_x86 = true\ninstruction_target_is_64bit = true"u8);
+ InstructionOperationStatus contradictory =
+ InstructionProfiles.TryObserveCurrent(out InstructionTargetProfile rejectedProfile);
+
+ Assert.Equal(InstructionOperationStatus.InvalidProfile, contradictory);
+ Assert.Equal(default, rejectedProfile);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void ObserveCurrent_uses_the_target_ARM_probe_and_matching_width_without_using_the_host_width()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+
+ EngineTest.Run(scope.State, "instruction_target_is_64bit = false\ninstruction_target_is_arm = true"u8);
+ InstructionTargetProfile arm32 = Observe(scope.State);
+
+ EngineTest.Run(scope.State, "instruction_target_is_64bit = true"u8);
+ InstructionTargetProfile arm64 = Observe(scope.State);
+
+ Assert.Equal(CheatEngineArchitecture.Arm32, arm32.Profile.Architecture);
+ Assert.Equal(PointerSize.Bit32, arm32.Profile.AddressWidth);
+ Assert.Equal(CheatEngineArchitecture.Arm64, arm64.Profile.Architecture);
+ Assert.Equal(PointerSize.Bit64, arm64.Profile.AddressWidth);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Assemble_passes_the_explicit_relative_origin_and_copies_the_complete_byte_table()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+ Span destination = stackalloc byte[5];
+ Address origin = 0x0000_0001_4000_1000UL;
+
+ InstructionOperationStatus status = InstructionAssembler.TryAssemble(targetProfile, "jmp rel", origin,
+ destination, out int written, out int requiredLength);
+
+ Assert.Equal(InstructionOperationStatus.Success, status);
+ Assert.Equal(5, written);
+ Assert.Equal(5, requiredLength);
+ Assert.Equal(new byte[] { 0xE9, 0xFB, 0xFF, 0xFF, 0xFF }, destination.ToArray());
+ Assert.Equal(origin.ToInt64(), ReadInteger(scope.State, "instruction_assemble_origin"));
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Assemble_rejection_capacity_and_malformed_tables_do_not_publish_a_prefix()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+
+ Span rejectedDestination = stackalloc byte[5];
+ rejectedDestination.Fill(0xA5);
+ InstructionOperationStatus rejected = InstructionAssembler.TryAssemble(targetProfile, "reject", 0x140001000UL,
+ rejectedDestination, out int rejectedWritten, out int rejectedRequired);
+
+ Assert.Equal(InstructionOperationStatus.InstructionRejected, rejected);
+ Assert.Equal(0, rejectedWritten);
+ Assert.Equal(0, rejectedRequired);
+ Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5, 0xA5 }, rejectedDestination.ToArray());
+
+ Span tooSmallDestination = stackalloc byte[4];
+ tooSmallDestination.Fill(0xA5);
+ InstructionOperationStatus tooSmall = InstructionAssembler.TryAssemble(targetProfile, "jmp rel", 0x140001000UL,
+ tooSmallDestination, out int tooSmallWritten, out int tooSmallRequired);
+
+ Assert.Equal(InstructionOperationStatus.DestinationTooSmall, tooSmall);
+ Assert.Equal(0, tooSmallWritten);
+ Assert.Equal(5, tooSmallRequired);
+ Assert.Equal(new byte[] { 0xA5, 0xA5, 0xA5, 0xA5 }, tooSmallDestination.ToArray());
+
+ Span malformedDestination = stackalloc byte[2];
+ malformedDestination.Fill(0xA5);
+ InstructionOperationStatus malformed = InstructionAssembler.TryAssemble(targetProfile, "malformed",
+ 0x140001000UL,
+ malformedDestination, out int malformedWritten, out int malformedRequired);
+
+ Assert.Equal(InstructionOperationStatus.InvalidResult, malformed);
+ Assert.Equal(0, malformedWritten);
+ Assert.Equal(0, malformedRequired);
+ Assert.Equal(new byte[] { 0xA5, 0xA5 }, malformedDestination.ToArray());
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Assemble_rejects_an_address_wider_than_the_observed_x86_profile_before_entering_Lua()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ EngineTest.Run(scope.State, "instruction_target_is_64bit = false\ninstruction_target_is_x86 = true"u8);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+ Span destination = stackalloc byte[5];
+
+ InstructionOperationStatus overwide = InstructionAssembler.TryAssemble(targetProfile, "jmp rel",
+ 0x1_0000_0000UL,
+ destination, out int written, out int requiredLength);
+
+ Assert.Equal(InstructionOperationStatus.AddressExceedsProfileWidth, overwide);
+ Assert.Equal(0, written);
+ Assert.Equal(0, requiredLength);
+ Assert.Equal(0, ReadInteger(scope.State, "instruction_assemble_calls"));
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Assemble_reports_a_target_change_without_copying_the_returned_bytes()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+ EngineTest.Run(scope.State, "instruction_assemble_behavior = \"target-change\""u8);
+ Span destination = stackalloc byte[1];
+ destination[0] = 0xA5;
+
+ InstructionOperationStatus status = InstructionAssembler.TryAssemble(targetProfile, "nop", 0x140001000UL,
+ destination, out int written, out int requiredLength);
+
+ Assert.Equal(InstructionOperationStatus.TargetChanged, status);
+ Assert.Equal(0, written);
+ Assert.Equal(0, requiredLength);
+ Assert.Equal(0xA5, destination[0]);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Disassemble_parses_the_fixture_line_inside_the_SDK_and_copies_all_text()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+ Address address = 0x0000_0001_4000_1000UL;
+
+ InstructionOperationStatus status = InstructionDisassembler.TryDisassemble(targetProfile, address, 256,
+ out InstructionDisassembly instruction, out int requiredUtf8Bytes);
+
+ Assert.Equal(InstructionOperationStatus.Success, status);
+ Assert.Equal(address, instruction.Address);
+ Assert.Equal("0000000140001000", instruction.AddressText);
+ Assert.Equal("E9 FB FF FF FF", instruction.Bytes);
+ Assert.Equal("jmp", instruction.Opcode);
+ Assert.Equal("0000000140001000", instruction.Extra);
+ Assert.Equal(49, instruction.Utf8ByteLength);
+ Assert.Equal(55, requiredUtf8Bytes);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Disassemble_enforces_the_raw_text_bound_and_rejects_malformed_split_results()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+
+ InstructionOperationStatus bounded = InstructionDisassembler.TryDisassemble(targetProfile, 0x140001000UL, 4,
+ out InstructionDisassembly boundedInstruction, out int requiredUtf8Bytes);
+ InstructionOperationStatus malformed = InstructionDisassembler.TryDisassemble(targetProfile, 0xBADUL, 256,
+ out InstructionDisassembly malformedInstruction, out int malformedRequiredUtf8Bytes);
+ InstructionOperationStatus oversizedFields = InstructionDisassembler.TryDisassemble(targetProfile, 0xBADDUL, 10,
+ out InstructionDisassembly oversizedInstruction, out int oversizedRequiredUtf8Bytes);
+
+ Assert.Equal(InstructionOperationStatus.OutputTooLong, bounded);
+ Assert.Equal(default, boundedInstruction);
+ Assert.Equal(55, requiredUtf8Bytes);
+ Assert.Equal(InstructionOperationStatus.InvalidResult, malformed);
+ Assert.Equal(default, malformedInstruction);
+ Assert.Equal(9, malformedRequiredUtf8Bytes);
+ Assert.Equal(InstructionOperationStatus.OutputTooLong, oversizedFields);
+ Assert.Equal(default, oversizedInstruction);
+ Assert.Equal(10, oversizedRequiredUtf8Bytes);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Missing_and_raising_instruction_globals_have_distinct_structured_outcomes()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using (HostScope missingScope = new(state))
+ {
+ InstallProfileGlobals(missingScope.State);
+ InstructionTargetProfile targetProfile = Observe(missingScope.State);
+ InstructionOperationStatus unavailable = InstructionDisassembler.TryDisassemble(targetProfile,
+ 0x140001000UL,
+ 256, out _, out _);
+
+ Assert.Equal(InstructionOperationStatus.GlobalUnavailable, unavailable);
+ Assert.Equal(0, missingScope.State.Top);
+ }
+
+ using HostScope raisingScope = new(state);
+ InstallInstructionGlobals(raisingScope.State);
+ InstructionTargetProfile raisingProfile = Observe(raisingScope.State);
+ InstructionOperationStatus failure = InstructionDisassembler.TryDisassemble(raisingProfile, 0xDEADUL,
+ 256, out _, out _);
+
+ Assert.Equal(InstructionOperationStatus.LuaFailure, failure);
+ Assert.Equal(0, raisingScope.State.Top);
+ }
+
+ [Fact]
+ public void Navigation_returns_a_positive_length_and_an_explicitly_estimated_previous_address()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile targetProfile = Observe(scope.State);
+
+ InstructionOperationStatus lengthStatus = InstructionNavigator.TryGetLength(targetProfile, 0x140001000UL,
+ out int length);
+ InstructionOperationStatus previousStatus = InstructionNavigator.TryGetPrevious(targetProfile, 0x140001000UL,
+ out Address previous);
+
+ Assert.Equal(InstructionOperationStatus.Success, lengthStatus);
+ Assert.Equal(5, length);
+ Assert.Equal(InstructionOperationStatus.Success, previousStatus);
+ Assert.Equal(0x140000FFBUL, previous.Value);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ [Fact]
+ public void Navigation_rejects_nonpositive_or_string_lengths_and_addresses_wider_than_the_profile()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ InstallInstructionGlobals(scope.State);
+ InstructionTargetProfile x64Profile = Observe(scope.State);
+
+ InstructionOperationStatus invalidLength =
+ InstructionNavigator.TryGetLength(x64Profile, 0UL, out int zeroLength);
+ InstructionOperationStatus stringLength =
+ InstructionNavigator.TryGetLength(x64Profile, 1UL, out int coercedLength);
+
+ EngineTest.Run(scope.State, "instruction_target_is_64bit = false\ninstruction_target_is_x86 = true"u8);
+ InstructionTargetProfile x86Profile = Observe(scope.State);
+ InstructionOperationStatus overwidePrevious = InstructionNavigator.TryGetPrevious(x86Profile, 0x1000UL,
+ out Address previous);
+
+ Assert.Equal(InstructionOperationStatus.InvalidResult, invalidLength);
+ Assert.Equal(0, zeroLength);
+ Assert.Equal(InstructionOperationStatus.InvalidResult, stringLength);
+ Assert.Equal(0, coercedLength);
+ Assert.Equal(InstructionOperationStatus.AddressExceedsProfileWidth, overwidePrevious);
+ Assert.Equal(Address.Zero, previous);
+ Assert.Equal(0, scope.State.Top);
+ }
+
+ private static InstructionTargetProfile Observe(LuaState state)
+ {
+ InstructionOperationStatus status =
+ InstructionProfiles.TryObserveCurrent(out InstructionTargetProfile targetProfile);
+ Assert.Equal(InstructionOperationStatus.Success, status);
+ Assert.Equal(0, state.Top);
+ return targetProfile;
+ }
+
+ private static void InstallInstructionGlobals(LuaState state)
+ {
+ InstallProfileGlobals(state);
+ EngineTest.Run(state, """
+ instruction_assemble_calls = 0
+ instruction_assemble_origin = 0
+ instruction_assemble_behavior = "normal"
+
+ assemble = function(line, address)
+ instruction_assemble_calls = instruction_assemble_calls + 1
+ instruction_assemble_origin = address
+ if instruction_assemble_behavior == "target-change" then
+ instruction_target_process_id = 7777
+ return { 0x90 }, nil
+ end
+ if line == "reject" then return nil, "instruction rejected" end
+ if line == "malformed" then return { 0x90, 300 }, nil end
+ if line == "raise" then error("assembly failure") end
+ return { 0xE9, 0xFB, 0xFF, 0xFF, 0xFF }, nil
+ end
+
+ disassemble = function(address)
+ if address == 0xDEAD then error("disassembly failure") end
+ if address == 0xBAD then return "bad-split" end
+ if address == 0xBADD then return "wide-split" end
+ return "0000000140001000 E9 FB FF FF FF jmp 0000000140001000"
+ end
+
+ splitDisassembledString = function(line)
+ if line == "bad-split" then return "only-one" end
+ if line == "wide-split" then return "aaaa", "bbbb", "cccc", "dddd" end
+ return "0000000140001000", "E9 FB FF FF FF", "jmp", "0000000140001000"
+ end
+
+ getInstructionSize = function(address)
+ if address == 0 then return 0 end
+ if address == 1 then return "5" end
+ return 5
+ end
+
+ getPreviousOpcode = function(address)
+ if address == 0x1000 then return 0x100000000 end
+ return address - 5
+ end
+ """u8);
+ }
+
+ private static void InstallProfileGlobals(LuaState state)
+ {
+ EngineTest.Run(state, """
+ instruction_target_process_id = 4242
+ instruction_target_is_64bit = true
+ instruction_target_is_x86 = false
+ instruction_target_is_arm = false
+ function getOpenedProcessID() return instruction_target_process_id end
+ function targetIs64Bit() return instruction_target_is_64bit end
+ function targetIsX86() return instruction_target_is_x86 end
+ function targetIsArm() return instruction_target_is_arm end
+ """u8);
+ }
+
+ private static long ReadInteger(LuaState state, string name)
+ {
+ using LuaFrame frame = new(state);
+ Assert.True(state.TryGetGlobal(Encoding.UTF8.GetBytes(name)).IsOk);
+ return EngineTest.ReadInteger(state, -1);
+ }
}
diff --git a/tests/CheatEngine.SDK.Engine.Tests/Inspection/EngineInspectionTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Inspection/EngineInspectionTests.cs
index 9e188b9b..9985a1d8 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/Inspection/EngineInspectionTests.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/Inspection/EngineInspectionTests.cs
@@ -206,7 +206,7 @@ public void ResolveAddress_distinguishes_a_nil_miss_from_a_zero_address_and_lua_
Assert.Equal(top, L.Top);
status = EngineInspection.ResolveHostAddress(new SymbolExpression("hostSymbol"),
- new AddressResolutionOptions(Shallow: true), out HostAddress found);
+ new AddressResolutionOptions(true), out HostAddress found);
Assert.Equal(InspectionStatus.Success, status);
Assert.Equal(unchecked((nuint) 0x7FF600001000UL), found.Value);
@@ -243,32 +243,13 @@ public void ResolveAddress_reports_an_unavailable_global_without_entering_lua()
}
[Fact]
- public void Legacy_positional_host_option_is_preserved_but_rejected_by_target_resolution()
- {
- AddressResolutionOptions legacy = new(true);
- legacy.Deconstruct(out bool useHostSymbolTable, out bool shallow);
-
- Assert.True(useHostSymbolTable);
- Assert.False(shallow);
- Assert.Throws(() =>
- EngineInspection.ResolveAddress(new SymbolExpression("hostSymbol"), legacy, out _));
- }
-
- [Fact]
- public void AddressResolutionOptions_preserves_init_and_with_compatibility()
+ public void AddressResolutionOptions_supports_init_and_with()
{
AddressResolutionOptions options = new() { Shallow = true };
AddressResolutionOptions updated = options with { Shallow = false };
Assert.True(options.Shallow);
Assert.False(updated.Shallow);
-
-#pragma warning disable CS0618
- AddressResolutionOptions legacy = new() { UseHostSymbolTable = true };
-#pragma warning restore CS0618
-#pragma warning disable CS0618
- Assert.True(legacy.UseHostSymbolTable);
-#pragma warning restore CS0618
}
[Fact]
diff --git a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs
index f238433f..cfeb2d2c 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/Inspection/SymbolRegistryTests.cs
@@ -1,6 +1,7 @@
using CheatEngine.SDK.Engine.Inspection;
using CheatEngine.SDK.Engine.Tests.Support;
using CheatEngine.SDK.Lua.Calls;
+using CheatEngine.SDK.Lua.Runtime;
using CheatEngine.SDK.Lua.State;
using CheatEngine.SDK.Tests.Shared.NativeLua;
@@ -146,6 +147,157 @@ public void Owned_registration_lease_never_unregisters_a_newer_coordinated_regis
Assert.Equal(0, L.Top);
}
+ [Fact]
+ public void Owned_registration_factory_failure_compensates_once_and_preserves_the_primary_cause()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ EngineTest.Run(L, """
+ registrations = 0
+ removals = 0
+ registerSymbol = function(name, address, doNotSave) registrations = registrations + 1 end
+ unregisterSymbol = function(name) removals = removals + 1 end
+ """u8);
+ InvalidOperationException cause = new("injected lease factory failure");
+
+ SymbolRegistrationHandoffException exception = Assert.Throws(() =>
+ SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default,
+ (_, _, _) => throw cause,
+ static (_, _) => throw new InvalidOperationException("The publisher must not run.")));
+
+ Assert.Same(cause, exception.InnerException);
+ Assert.Equal(SymbolRegistrationReleaseKind.Released, exception.CleanupOutcome.Kind);
+ Assert.True(exception.CleanupOutcome.Status.IsSuccess);
+ EngineTest.Run(L, "assert(registrations == 1 and removals == 1)"u8);
+ Assert.Equal(0, L.Top);
+ }
+
+ [Fact]
+ public void Owned_registration_publish_failure_compensates_once_and_preserves_the_primary_cause()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ EngineTest.Run(L, """
+ registrations = 0
+ removals = 0
+ registerSymbol = function(name, address, doNotSave) registrations = registrations + 1 end
+ unregisterSymbol = function(name) removals = removals + 1 end
+ """u8);
+ InvalidOperationException cause = new("injected lease publication failure");
+
+ SymbolRegistrationHandoffException exception = Assert.Throws(() =>
+ SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default,
+ static (name, options, identity) => new SymbolRegistrationLease(name, options, identity),
+ (_, _) => throw cause));
+
+ Assert.Same(cause, exception.InnerException);
+ Assert.Equal(SymbolRegistrationReleaseKind.Released, exception.CleanupOutcome.Kind);
+ Assert.True(exception.CleanupOutcome.Status.IsSuccess);
+ EngineTest.Run(L, "assert(registrations == 1 and removals == 1)"u8);
+ Assert.Equal(0, L.Top);
+ }
+
+ [Fact]
+ public void Owned_registration_publication_failure_after_detach_preserves_the_primary_cause_without_unregistering()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ EngineTest.Run(L, """
+ registrations = 0
+ removals = 0
+ registerSymbol = function(name, address, doNotSave) registrations = registrations + 1 end
+ unregisterSymbol = function(name) removals = removals + 1 end
+ """u8);
+ InvalidOperationException cause = new("injected lease publication failure after detach");
+
+ SymbolRegistrationHandoffException exception;
+ try
+ {
+ exception = Assert.Throws(() =>
+ SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default,
+ static (name, options, identity) => new SymbolRegistrationLease(name, options, identity),
+ (_, _) =>
+ {
+ LuaRuntime.Detach();
+ throw cause;
+ }));
+ }
+ finally
+ {
+ LuaRuntime.Attach(scope.Binding);
+ }
+
+ Assert.Same(cause, exception.InnerException);
+ Assert.Equal(SymbolRegistrationReleaseKind.StaleRuntime, exception.CleanupOutcome.Kind);
+ Assert.True(exception.CleanupOutcome.Status.IsSuccess);
+ Assert.True(exception.CleanupOutcome.IsTerminal);
+ EngineTest.Run(L, "assert(registrations == 1 and removals == 0)"u8);
+ Assert.Equal(0, L.Top);
+ }
+
+ [Fact]
+ public void Owned_registration_publication_failure_with_unavailable_unregister_preserves_the_primary_cause()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ EngineTest.Run(L, """
+ registrations = 0
+ registerSymbol = function(name, address, doNotSave) registrations = registrations + 1 end
+ """u8);
+ InvalidOperationException cause = new("injected lease publication failure");
+
+ SymbolRegistrationHandoffException exception = Assert.Throws(() =>
+ SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default,
+ static (name, options, identity) => new SymbolRegistrationLease(name, options, identity),
+ (_, _) => throw cause));
+
+ Assert.Same(cause, exception.InnerException);
+ Assert.Equal(SymbolRegistrationReleaseKind.CleanupUnavailable, exception.CleanupOutcome.Kind);
+ Assert.Equal(LuaOperationStatusKind.GlobalUnavailable, exception.CleanupOutcome.Status.Kind);
+ Assert.False(exception.CleanupOutcome.IsTerminal);
+ EngineTest.Run(L, "assert(registrations == 1)"u8);
+ Assert.Equal(0, L.Top);
+ }
+
+ [Fact]
+ public void Owned_registration_compensation_failure_is_indeterminate_and_never_retries()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ EngineTest.Run(L, """
+ registrations = 0
+ removals = 0
+ registerSymbol = function(name, address, doNotSave) registrations = registrations + 1 end
+ unregisterSymbol = function(name)
+ removals = removals + 1
+ error('cleanup started then failed')
+ end
+ """u8);
+ InvalidOperationException cause = new("injected lease factory failure");
+
+ SymbolRegistrationHandoffException exception = Assert.Throws(() =>
+ SymbolRegistry.TryRegisterOwnedCore(new SymbolName("Player.Health"), 0x140001000UL, default,
+ (_, _, _) => throw cause,
+ static (_, _) => throw new InvalidOperationException("The publisher must not run.")));
+
+ Assert.Same(cause, exception.InnerException);
+ Assert.Equal(SymbolRegistrationReleaseKind.CleanupIndeterminate, exception.CleanupOutcome.Kind);
+ Assert.Equal(LuaOperationStatusKind.LuaFailure, exception.CleanupOutcome.Status.Kind);
+ Assert.True(exception.CleanupOutcome.IsTerminal);
+ EngineTest.Run(L, "assert(registrations == 1 and removals == 1)"u8);
+ Assert.Equal(0, L.Top);
+ }
+
[Fact]
public void Symbol_mutations_that_raise_after_starting_supersede_tracked_leases()
{
@@ -273,7 +425,7 @@ public void Owned_registration_cleanup_after_a_lua_failure_is_indeterminate_and_
LuaState L = scope.State;
EngineTest.Run(L,
- "registerSymbol = function(name, address, doNotSave) end; unregisterSymbol = function(name) error('cleanup started then failed') end"u8);
+ "removals = 0; registerSymbol = function(name, address, doNotSave) end; unregisterSymbol = function(name) removals = removals + 1; error('cleanup started then failed') end"u8);
SymbolRegistrationAcquireOutcome failed =
SymbolRegistry.TryRegisterOwned(new SymbolName("Player.Mana"), 0x140002000UL);
SymbolRegistrationReleaseOutcome indeterminate = failed.Lease!.Release();
@@ -283,6 +435,34 @@ public void Owned_registration_cleanup_after_a_lua_failure_is_indeterminate_and_
Assert.False(indeterminate.Status.IsSuccess);
Assert.True(failed.Lease.IsTerminal);
Assert.Equal(SymbolRegistrationReleaseKind.AlreadyReleased, failed.Lease.Release().Kind);
+ failed.Lease.Dispose();
+ failed.Lease.Dispose();
+ EngineTest.Run(L, "assert(removals == 1)"u8);
+ Assert.Equal(0, L.Top);
+ }
+
+ [Fact]
+ public void Owned_registration_dispose_is_no_throw_idempotent_and_unregisters_once()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ EngineTest.Run(L, """
+ removals = 0
+ registerSymbol = function(name, address, doNotSave) end
+ unregisterSymbol = function(name) removals = removals + 1 end
+ """u8);
+ SymbolRegistrationAcquireOutcome acquired =
+ SymbolRegistry.TryRegisterOwned(new SymbolName("Player.Health"), 0x140001000UL);
+
+ Assert.True(acquired.HasLease);
+ acquired.Lease!.Dispose();
+ acquired.Lease.Dispose();
+
+ Assert.True(acquired.Lease.IsTerminal);
+ Assert.Equal(SymbolRegistrationReleaseKind.AlreadyReleased, acquired.Lease.Release().Kind);
+ EngineTest.Run(L, "assert(removals == 1)"u8);
Assert.Equal(0, L.Top);
}
}
diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs
index 0a3f4c2c..c419e50b 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionFactoryTests.cs
@@ -1,7 +1,9 @@
+using System.Globalization;
using System.Text;
using CheatEngine.SDK.Engine.Objects;
using CheatEngine.SDK.Engine.Scanning.Values;
+using CheatEngine.SDK.Engine.Targets;
using CheatEngine.SDK.Engine.Tests.Support;
using CheatEngine.SDK.Lua.State;
using CheatEngine.SDK.Tests.Shared.NativeLua;
@@ -13,17 +15,24 @@ namespace CheatEngine.SDK.Engine.Tests.Scanning;
public sealed class MemoryScanSessionFactoryTests
{
[Fact]
- public void TryCreate_when_both_factories_return_host_objects_transfers_ownership_to_the_session()
+ public void TryCreateWithOutcome_when_both_factories_return_host_objects_transfers_ownership_to_the_session()
{
EngineTest.RequireNativeLua();
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
CEObject scanner = CreateScanner(L);
CEObject foundList = CreateFoundList(L);
InstallFactories(L, scanner, foundList);
- Assert.True(MemoryScanSessions.TryCreate(out MemoryScanSession? created));
+ MemoryScanCreationOutcome outcome = MemoryScanSessions.TryCreateWithOutcome(out MemoryScanSession? created);
+
+ Assert.Equal(MemoryScanCreationStatus.Success, outcome.Status);
+ Assert.Equal(TargetSelectionObservationStatus.CurrentTargetQualified, outcome.TargetObservation.Status);
+ Assert.Equal(Environment.ProcessId, outcome.TargetObservation.SelectedProcessId);
+ Assert.True(outcome.TargetObservation.Incarnation.HasValue);
+ Assert.Equal(Environment.ProcessId, outcome.TargetObservation.Incarnation.Value.ProcessId);
MemoryScanSession session = Assert.IsType(created);
Assert.Equal(scanner, session.Scanner.Handle);
Assert.Equal(0, L.Top);
@@ -43,6 +52,7 @@ public void TryCreate_when_the_child_factory_is_unavailable_rolls_back_the_creat
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
CEObject scanner = CreateScanner(L);
SetGlobalObject(L, "factory_scan"u8, scanner);
EngineTest.Run(L,
@@ -64,6 +74,7 @@ public void TryCreate_when_the_child_factory_raises_rolls_back_the_created_paren
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
CEObject scanner = CreateScanner(L);
SetGlobalObject(L, "factory_scan"u8, scanner);
EngineTest.Run(L, """
@@ -94,6 +105,7 @@ public void TryCreate_when_the_child_factory_returns_a_nonobject_rolls_back_the_
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
CEObject scanner = CreateScanner(L);
SetGlobalObject(L, "factory_scan"u8, scanner);
EngineTest.Run(L, """
@@ -123,6 +135,7 @@ public void TryCreate_when_the_child_factory_aliases_the_parent_rolls_back_witho
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
CEObject scanner = CreateScanner(L);
SetGlobalObject(L, "factory_scan"u8, scanner);
EngineTest.Run(L, """
@@ -175,6 +188,7 @@ public void TryCreate_when_the_parent_factory_returns_a_nonobject_does_not_publi
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
EngineTest.Run(L, "function createMemScan() return 42 end"u8);
Assert.Equal(MemoryScanCreationStatus.InvalidScannerResult,
@@ -190,6 +204,7 @@ public void TryCreateDetailed_when_a_factory_returns_nil_keeps_absence_distinct_
using NativeLuaState state = new();
using HostScope scope = new(state);
LuaState L = scope.State;
+ InstallCurrentTarget(L);
EngineTest.Run(L, "function createMemScan() return nil end"u8);
MemoryScanCreationStatus status = MemoryScanSessions.TryCreateDetailed(out MemoryScanSession? created);
@@ -271,6 +286,35 @@ function getOpenedProcessID()
Assert.Equal(0, L.Top);
}
+ [Fact]
+ public void TryCreateWithOutcome_refuses_an_unqualified_target_before_either_factory_acquires_an_owner()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ LuaState L = scope.State;
+ CEObject scanner = CreateScanner(L);
+ CEObject foundList = CreateFoundList(L);
+ SetGlobalObject(L, "factory_scan"u8, scanner);
+ SetGlobalObject(L, "factory_found_list"u8, foundList);
+ EngineTest.Run(L, """
+ trace = {}
+ function getOpenedProcessID() return 0 end
+ function createMemScan() table.insert(trace, 'factory.scan'); return factory_scan end
+ function createFoundList(scan) table.insert(trace, 'factory.list'); return factory_found_list end
+ """u8);
+
+ MemoryScanCreationOutcome outcome = MemoryScanSessions.TryCreateWithOutcome(out MemoryScanSession? created);
+
+ Assert.Equal(MemoryScanCreationStatus.TargetIdentityUnavailable, outcome.Status);
+ Assert.Equal(TargetSelectionObservationStatus.NoTargetSelected, outcome.TargetObservation.Status);
+ Assert.Null(created);
+ Assert.False(FakeHost.IsDestroyed(L, scanner));
+ Assert.False(FakeHost.IsDestroyed(L, foundList));
+ Assert.Equal(string.Empty, ReadTrace(L));
+ Assert.Equal(0, L.Top);
+ }
+
private static CEObject CreateScanner(LuaState state)
{
return FakeHost.CreateObject(state, "Object", """
@@ -315,6 +359,13 @@ function getOpenedProcessID()
"""));
}
+ private static void InstallCurrentTarget(LuaState state)
+ {
+ EngineTest.Run(state, Encoding.UTF8.GetBytes("function getOpenedProcessID() return " +
+ Environment.ProcessId.ToString(CultureInfo.InvariantCulture) +
+ " end"));
+ }
+
private static void SetGlobalObject(LuaState state, ReadOnlySpan name, CEObject value)
{
using LuaFrame frame = new(state);
diff --git a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionTests.cs b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionTests.cs
index 1963f1fd..630bfdd5 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionTests.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/Scanning/MemoryScanSessionTests.cs
@@ -3,6 +3,7 @@
using System.Text;
using CheatEngine.SDK.Engine.Enums;
+using CheatEngine.SDK.Engine.Errors;
using CheatEngine.SDK.Engine.Objects;
using CheatEngine.SDK.Engine.Scanning.Values;
using CheatEngine.SDK.Engine.Targets;
@@ -119,7 +120,7 @@ public void Wait_error_invalidates_the_session_so_reset_can_recover()
}
[Fact]
- public void Dispose_releases_and_destroys_the_child_before_the_parent()
+ public void ReleaseWithOutcome_confirms_and_consumes_the_child_before_the_parent_without_a_retry()
{
EngineTest.RequireNativeLua();
using NativeLuaState state = new();
@@ -130,16 +131,20 @@ public void Dispose_releases_and_destroys_the_child_before_the_parent()
session.WaitForCompletion();
EngineTest.Run(scope.State, "trace = {}"u8);
- session.Dispose();
- session.Dispose();
+ MemoryScanReleaseOutcome outcome = session.ReleaseWithOutcome();
+ MemoryScanReleaseOutcome repeated = session.ReleaseWithOutcome();
Assert.Equal(MemoryScanState.Disposed, session.State);
+ Assert.True(outcome.OwnershipConsumed);
+ Assert.Equal(TargetReleaseStatus.Released, outcome.FoundList.Status);
+ Assert.Equal(TargetReleaseStatus.Released, outcome.MemScan.Status);
+ Assert.Equal(outcome, repeated);
Assert.Equal("list.deinitialize,list.destroy,scan.destroy", ReadTrace(scope.State));
Assert.Throws(() => _ = session.Scanner);
}
[Fact]
- public void Dispose_attempts_the_parent_cleanup_after_an_unconfirmed_child_destroy()
+ public void ReleaseWithOutcome_reports_unknown_child_cleanup_and_still_attempts_the_parent_once()
{
EngineTest.RequireNativeLua();
using NativeLuaState state = new();
@@ -150,15 +155,19 @@ public void Dispose_attempts_the_parent_cleanup_after_an_unconfirmed_child_destr
CEObject scanner = session.Scanner.Handle;
EngineTest.Run(scope.State, "trace = {}"u8);
- session.Dispose();
+ MemoryScanReleaseOutcome outcome = session.ReleaseWithOutcome();
Assert.Equal(MemoryScanState.Disposed, session.State);
+ Assert.True(outcome.OwnershipConsumed);
+ Assert.Equal(TargetReleaseStatus.UnconfirmedAfterInvocation, outcome.FoundList.Status);
+ Assert.Equal(EngineFailureKind.ProtectedLuaFailure, outcome.FoundList.FailureKind);
+ Assert.Equal(TargetReleaseStatus.Released, outcome.MemScan.Status);
Assert.True(FakeHost.IsDestroyed(scope.State, scanner));
Assert.Equal("list.deinitialize,list.destroy,scan.destroy", ReadTrace(scope.State));
}
[Fact]
- public void Dispose_after_a_detach_and_reattach_refuses_to_use_the_prior_runtime_and_can_be_abandoned()
+ public void Dispose_after_a_detach_consumes_ownership_without_throwing_or_using_the_prior_runtime()
{
EngineTest.RequireNativeLua();
using NativeLuaState state = new();
@@ -170,11 +179,16 @@ public void Dispose_after_a_detach_and_reattach_refuses_to_use_the_prior_runtime
CEObject scanner = session.Scanner.Handle;
CEObject foundList = session.Results.Handle;
+ MemoryScanReleaseOutcome outcome;
LuaRuntime.Detach();
try
{
- Assert.Throws(session.Dispose);
- Assert.Equal(MemoryScanState.ResultsReady, session.State);
+ session.Dispose();
+ outcome = session.LastReleaseOutcome;
+ Assert.Equal(MemoryScanState.Disposed, session.State);
+ Assert.True(outcome.OwnershipConsumed);
+ Assert.Equal(TargetReleaseStatus.NotInvoked, outcome.FoundList.Status);
+ Assert.Equal(TargetReleaseStatus.NotInvoked, outcome.MemScan.Status);
Assert.False(FakeHost.IsDestroyed(scope.State, scanner));
Assert.False(FakeHost.IsDestroyed(scope.State, foundList));
}
@@ -183,11 +197,6 @@ public void Dispose_after_a_detach_and_reattach_refuses_to_use_the_prior_runtime
LuaRuntime.Attach(scope.Binding);
}
- MemoryScanException invalidated = Assert.Throws(session.Dispose);
-
- Assert.Equal(MemoryScanFailureKind.RuntimeInvalidated, invalidated.FailureKind);
- Assert.Equal(MemoryScanState.Invalidated, session.State);
- session.Abandon();
Assert.Equal(MemoryScanState.Disposed, session.State);
Assert.False(FakeHost.IsDestroyed(scope.State, foundList));
Assert.False(FakeHost.IsDestroyed(scope.State, scanner));
@@ -315,7 +324,7 @@ public void A_main_thread_only_scan_operation_is_rejected_before_it_touches_CE()
}
[Fact]
- public void Disposal_on_a_worker_is_rejected_before_child_or_parent_cleanup()
+ public void Disposal_on_a_worker_never_throws_or_retries_unsafe_child_or_parent_cleanup()
{
EngineTest.RequireNativeLua();
using NativeLuaState state = new();
@@ -328,12 +337,12 @@ public void Disposal_on_a_worker_is_rejected_before_child_or_parent_cleanup()
Exception? failure = EngineTest.RunOnWorker(session.Dispose);
- InvalidOperationException exception = Assert.IsType(failure);
- Assert.Contains("main thread", exception.Message, StringComparison.Ordinal);
- Assert.Equal(MemoryScanState.ResultsReady, session.State);
+ Assert.Null(failure);
+ Assert.Equal(MemoryScanState.Disposed, session.State);
+ Assert.True(session.LastReleaseOutcome.OwnershipConsumed);
+ Assert.Equal(TargetReleaseStatus.NotInvoked, session.LastReleaseOutcome.FoundList.Status);
+ Assert.Equal(TargetReleaseStatus.NotInvoked, session.LastReleaseOutcome.MemScan.Status);
Assert.Equal(string.Empty, ReadTrace(scope.State));
-
- session.Dispose();
}
[Fact]
@@ -521,6 +530,197 @@ public void TryCopyResults_reports_a_preexisting_cancellation_without_reading_or
Assert.Equal(string.Empty, ReadTrace(scope.State));
}
+ [Fact]
+ public void TryCopyResults_cancels_after_get_address_without_reading_a_value_or_publishing_a_snapshot()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State);
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult retained = new(new Address(0xA11CE), "retained");
+ MemoryScanResult[] destination = [retained, retained];
+ using CancellationTokenSource cancellation = new();
+ using FakeHost.PCallProbe probe = FakeHost.ReplaceFoundListGetAddressWithPCallProbe(scope.State,
+ session.Results.Handle,
+ cancellation.Cancel);
+
+ MemoryScanMaterializationStatus status = CopyWithCancellation(session, destination, cancellation.Token,
+ out ulong totalCount, out int written);
+
+ Assert.Equal(MemoryScanMaterializationStatus.Cancelled, status);
+ Assert.Equal(2UL, totalCount);
+ Assert.Equal(0, written);
+ Assert.Equal(retained, destination[0]);
+ Assert.Equal(retained, destination[1]);
+ Assert.Equal(1, probe.GetAddressCallCount);
+ Assert.Equal(MemoryScanCancellationMilestone.ObservedAfterNativeCall, session.LastCancellationMilestone);
+ Assert.Equal("results.getCount", ReadTrace(scope.State));
+ }
+
+ [Fact]
+ public void TryCopyResultsPage_copies_only_the_requested_bounded_page_without_materializing_the_full_result_set()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State, resultCountLiteral: "3");
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult[] destination = new MemoryScanResult[1];
+
+ MemoryScanMaterializationStatus status = session.TryCopyResultsPage(1, destination, out ulong totalCount,
+ out int written);
+
+ Assert.Equal(MemoryScanMaterializationStatus.Success, status);
+ Assert.Equal(3UL, totalCount);
+ Assert.Equal(1, written);
+ Assert.Equal(new MemoryScanResult(new Address(0xFFFF_FFFF_FFFF_FFFF), "100"), destination[0]);
+ Assert.Equal("results.getCount,results.getAddress:1,results.getValue:1", ReadTrace(scope.State));
+ }
+
+ [Fact]
+ public void TryCopyResultsPage_does_not_publish_a_prefix_when_a_page_row_is_invalid()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State, invalidAddress: true);
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult retained = new(new Address(0xA11CE), "retained");
+ MemoryScanResult[] destination = [retained];
+
+ MemoryScanMaterializationStatus status = session.TryCopyResultsPage(0, destination, out ulong totalCount,
+ out int written);
+
+ Assert.Equal(MemoryScanMaterializationStatus.InvalidResult, status);
+ Assert.Equal(2UL, totalCount);
+ Assert.Equal(0, written);
+ Assert.Equal(retained, destination[0]);
+ Assert.Equal("results.getCount,results.getAddress:0", ReadTrace(scope.State));
+ }
+
+ [Fact]
+ public void TryCopyResultsPage_reports_no_results_without_touching_the_destination()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State, resultCountLiteral: "0");
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult retained = new(new Address(0xA11CE), "retained");
+ MemoryScanResult[] destination = [retained];
+
+ MemoryScanMaterializationStatus status = session.TryCopyResultsPage(0, destination, out ulong totalCount,
+ out int written);
+
+ Assert.Equal(MemoryScanMaterializationStatus.NoResults, status);
+ Assert.Equal(0UL, totalCount);
+ Assert.Equal(0, written);
+ Assert.Equal(retained, destination[0]);
+ Assert.Equal("results.getCount", ReadTrace(scope.State));
+ }
+
+ [Fact]
+ public void TryCopyResultsPage_rejects_an_exact_end_start_without_reading_a_row()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State);
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult retained = new(new Address(0xA11CE), "retained");
+ MemoryScanResult[] destination = [retained];
+
+ MemoryScanMaterializationStatus status = session.TryCopyResultsPage(2, destination, out ulong totalCount,
+ out int written);
+
+ Assert.Equal(MemoryScanMaterializationStatus.PageStartOutOfRange, status);
+ Assert.Equal(2UL, totalCount);
+ Assert.Equal(0, written);
+ Assert.Equal(retained, destination[0]);
+ Assert.Equal("results.getCount", ReadTrace(scope.State));
+ }
+
+ [Fact]
+ public void TryCopyResultsPage_rejects_zero_capacity_without_reading_a_row()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State);
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+
+ MemoryScanMaterializationStatus status = session.TryCopyResultsPage(0, [], out ulong totalCount,
+ out int written);
+
+ Assert.Equal(MemoryScanMaterializationStatus.DestinationTooSmall, status);
+ Assert.Equal(2UL, totalCount);
+ Assert.Equal(0, written);
+ Assert.Equal("results.getCount", ReadTrace(scope.State));
+ }
+
+ [Fact]
+ public void TryCopyResultsPage_rejects_a_negative_start_before_reading_the_count()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State);
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult retained = new(new Address(0xA11CE), "retained");
+ MemoryScanResult[] destination = [retained];
+
+ Assert.Throws(() =>
+ {
+ session.TryCopyResultsPage(-1, destination, out _, out _);
+ });
+
+ Assert.Equal(retained, destination[0]);
+ Assert.Equal(string.Empty, ReadTrace(scope.State));
+ }
+
+ [Fact]
+ [SuppressMessage("xUnit.Analyzers", "xUnit1051",
+ Justification = "The fixture starts with a deliberately cancelled token to prove no paged-row call begins.")]
+ public void TryCopyResultsPage_does_not_publish_a_prefix_when_cancellation_precedes_the_page()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ using MemoryScanSession session = CreateSession(scope.State);
+ session.StartFirstScan(FirstScanRequest.ExactValue(VariableType.Dword, "100"));
+ session.WaitForCompletion();
+ EngineTest.Run(scope.State, "trace = {}"u8);
+ MemoryScanResult retained = new(new Address(0xA11CE), "retained");
+ MemoryScanResult[] destination = [retained];
+ using CancellationTokenSource cancellation = new();
+ cancellation.Cancel();
+
+ MemoryScanMaterializationStatus status = session.TryCopyResultsPageCancellable(0, destination,
+ out ulong totalCount, out int written, cancellation.Token);
+
+ Assert.Equal(MemoryScanMaterializationStatus.Cancelled, status);
+ Assert.Equal(0UL, totalCount);
+ Assert.Equal(0, written);
+ Assert.Equal(retained, destination[0]);
+ Assert.Equal(MemoryScanCancellationMilestone.CancelledBeforeNativeCall, session.LastCancellationMilestone);
+ Assert.Equal(string.Empty, ReadTrace(scope.State));
+ }
+
[Fact]
[SuppressMessage("xUnit.Analyzers", "xUnit1051",
Justification = "The fixture must start with a deliberately cancelled token to prove no CE scan call begins.")]
@@ -582,6 +782,25 @@ public void A_session_refuses_scan_work_when_the_original_target_is_no_longer_se
session.Abandon();
}
+ [Fact]
+ public void ReleaseWithOutcome_refuses_cleanup_on_a_replaced_target_and_consumes_both_owners()
+ {
+ EngineTest.RequireNativeLua();
+ using NativeLuaState state = new();
+ using HostScope scope = new(state);
+ MemoryScanSession session = CreateSession(scope.State);
+ EngineTest.Run(scope.State, "opened_process_id = 0; trace = {}"u8);
+
+ MemoryScanReleaseOutcome outcome = session.ReleaseWithOutcome();
+
+ Assert.Equal(MemoryScanState.Disposed, session.State);
+ Assert.True(outcome.OwnershipConsumed);
+ Assert.Equal(TargetReleaseStatus.RefusedNoTarget, outcome.FoundList.Status);
+ Assert.Equal(TargetReleaseStatus.RefusedNoTarget, outcome.MemScan.Status);
+ Assert.Equal(TargetIdentityCheckKind.NoTargetSelected, outcome.FoundList.TargetCheck!.Value.Kind);
+ Assert.Equal(string.Empty, ReadTrace(scope.State));
+ }
+
private static MemoryScanMaterializationStatus CopyWithCancellation(MemoryScanSession session,
Span destination, CancellationToken cancellationToken, out ulong totalCount, out int written)
{
diff --git a/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.cs b/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.cs
index 4b9e099a..8e752300 100644
--- a/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.cs
+++ b/tests/CheatEngine.SDK.Engine.Tests/Support/FakeHost.cs
@@ -30,7 +30,7 @@ namespace CheatEngine.SDK.Engine.Tests.Support;
/// state lives in a Lua table stored as the userdata's user value (lua_setuservalue), keyed by pointer in a
/// registry table so that every push of the same pointer finds the same state.
///
-internal static unsafe class FakeHost
+internal static unsafe class FakeHost // NOSONAR: the fixture implements Cheat Engine's unmanaged callback ABI.
{
// Registry keys: light userdata whose values are the addresses of these bytes (stable for the process).
private const int MetatableKey = 0;
@@ -212,6 +212,21 @@ public static PCallProbe ReplaceWaitTillDoneWithPCallProbe(LuaState L, CEObject
return new PCallProbe(afterWaitTillDone);
}
+ /// Replaces a fake found-list's getAddress member with a callback that returns a valid address.
+ public static PCallProbe ReplaceFoundListGetAddressWithPCallProbe(LuaState L, CEObject foundList,
+ Action? afterGetAddress = null)
+ {
+ using LuaFrame frame = new(L);
+ Assert.Equal(LuaType.Table, L.RawGetPointer(LuaState.RegistryIndex, s_keys + ObjectsKey));
+ Assert.Equal(LuaType.Table, L.RawGetPointer(-1, foundList.Value));
+ Assert.True(L.TryGetField(-1, "props"u8).IsOk);
+ L.PushString("getAddress"u8);
+ L.PushUncheckedFunction(
+ new LuaNativeFunction((nint) (delegate* unmanaged[Cdecl]) &GetAddress));
+ Assert.True(L.TryRawSet(-3));
+ return new PCallProbe(null, afterGetAddress);
+ }
+
private static void Install(LuaState L)
{
using LuaFrame frame = new(L);
@@ -276,24 +291,16 @@ private static int WaitTillDone(lua_State* state)
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
- private static int ObservePCall(lua_State* state, int argumentCount, int resultCount, int errorFunction,
- nint context, nint continuation)
+ private static int GetAddress(lua_State* state)
{
- PCallProbe? probe = s_activePCallProbe;
- if (probe is not null &&
- (nint) lua_tocfunction(state, -argumentCount - 1) ==
- (nint) (delegate* unmanaged[Cdecl]) &WaitTillDone)
+ s_activePCallProbe?.AfterGetAddress();
+ ReadOnlySpan address = "00001234"u8;
+ fixed (byte* addressPointer = address)
{
- probe.Observe(argumentCount, resultCount);
+ _ = lua_pushlstring(state, addressPointer, (nuint) address.Length);
}
- return ((delegate* unmanaged[Cdecl]) s_forwardedPCall)(
- state,
- argumentCount,
- resultCount,
- errorFunction,
- context,
- continuation);
+ return 1;
}
private sealed class StateProviderSuppression : IDisposable
@@ -307,11 +314,12 @@ public void Dispose()
/// Captures protected calls specifically to the fake no-result waitTillDone function.
internal sealed class PCallProbe : IDisposable
{
+ private readonly Action? _afterGetAddress;
private readonly Action? _afterWaitTillDone;
private readonly FieldInfo _pcallField;
private readonly object _table;
- internal PCallProbe(Action? afterWaitTillDone)
+ internal PCallProbe(Action? afterWaitTillDone, Action? afterGetAddress = null)
{
if (s_activePCallProbe is not null)
{
@@ -332,9 +340,17 @@ internal PCallProbe(Action? afterWaitTillDone)
(nint) (delegate* unmanaged[Cdecl]) &ObservePCall);
tableField.SetValue(null, _table);
s_activePCallProbe = this;
+ _afterGetAddress = afterGetAddress;
_afterWaitTillDone = afterWaitTillDone;
}
+ /// Gets how many calls reached the probe's replacement getAddress function.
+ public int GetAddressCallCount
+ {
+ get;
+ private set;
+ }
+
/// Gets how many protected calls reached the probe's waitTillDone function.
public int WaitCallCount
{
@@ -382,5 +398,32 @@ internal void AfterWaitTillDone()
{
_afterWaitTillDone?.Invoke();
}
+
+ internal void AfterGetAddress()
+ {
+ GetAddressCallCount++;
+ _afterGetAddress?.Invoke();
+ }
+
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
+ private static int ObservePCall(lua_State* state, int argumentCount, int resultCount, int errorFunction,
+ nint context, nint continuation)
+ {
+ PCallProbe? probe = s_activePCallProbe;
+ if (probe is not null &&
+ (nint) lua_tocfunction(state, -argumentCount - 1) ==
+ (nint) (delegate* unmanaged[Cdecl]) &WaitTillDone)
+ {
+ probe.Observe(argumentCount, resultCount);
+ }
+
+ return ((delegate* unmanaged[Cdecl]) s_forwardedPCall)(
+ state,
+ argumentCount,
+ resultCount,
+ errorFunction,
+ context,
+ continuation);
+ }
}
}
diff --git a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/CoexistencePluginA.cs b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/CoexistencePluginA.cs
index e4147526..5446c428 100644
--- a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/CoexistencePluginA.cs
+++ b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginA/CoexistencePluginA.cs
@@ -1,6 +1,7 @@
using CheatEngine.SDK.Annotations.Plugin;
using CheatEngine.SDK.Hosting.Diagnostics;
using CheatEngine.SDK.Hosting.Plugin;
+using CheatEngine.SDK.Lua.Calls;
using CheatEngine.SDK.Lua.Runtime;
namespace LivePlugin.Coexistence.PluginA;
@@ -16,7 +17,7 @@ protected override void OnEnable()
{
CoexistenceDiagnostics.LogEnabled("A", typeof(CoexistencePluginA).Assembly, Context);
- var result = CoexistencePluginAFunctions.RegisterLuaFunctions(LuaRuntime.AcquireState());
+ LuaStatus result = CoexistencePluginAFunctions.RegisterLuaFunctions(LuaRuntime.AcquireState());
HostLog.Write(result.IsOk ? HostLogLevel.Information : HostLogLevel.Error,
"CheatEngine.SDK coexistence A: RegisterLuaFunctions -> " + result + ".");
if (!result.IsOk)
@@ -28,7 +29,7 @@ protected override void OnEnable()
///
protected override void OnDisable()
{
- var result = CoexistencePluginAFunctions.UnregisterLuaFunctions(LuaRuntime.AcquireState());
+ LuaStatus result = CoexistencePluginAFunctions.UnregisterLuaFunctions(LuaRuntime.AcquireState());
HostLog.Write(result.IsOk ? HostLogLevel.Information : HostLogLevel.Error,
"CheatEngine.SDK coexistence A: UnregisterLuaFunctions -> " + result + ".");
}
diff --git a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/CoexistencePluginB.cs b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/CoexistencePluginB.cs
index 0ef3af2f..61ef2f7f 100644
--- a/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/CoexistencePluginB.cs
+++ b/tests/CheatEngine.SDK.LivePlugin.Coexistence/PluginB/CoexistencePluginB.cs
@@ -1,6 +1,7 @@
using CheatEngine.SDK.Annotations.Plugin;
using CheatEngine.SDK.Hosting.Diagnostics;
using CheatEngine.SDK.Hosting.Plugin;
+using CheatEngine.SDK.Lua.Calls;
using CheatEngine.SDK.Lua.Runtime;
namespace LivePlugin.Coexistence.PluginB;
@@ -16,7 +17,7 @@ protected override void OnEnable()
{
CoexistenceDiagnostics.LogEnabled("B", typeof(CoexistencePluginB).Assembly, Context);
- var result = CoexistencePluginBFunctions.RegisterLuaFunctions(LuaRuntime.AcquireState());
+ LuaStatus result = CoexistencePluginBFunctions.RegisterLuaFunctions(LuaRuntime.AcquireState());
HostLog.Write(result.IsOk ? HostLogLevel.Information : HostLogLevel.Error,
"CheatEngine.SDK coexistence B: RegisterLuaFunctions -> " + result + ".");
if (!result.IsOk)
@@ -28,7 +29,7 @@ protected override void OnEnable()
///
protected override void OnDisable()
{
- var result = CoexistencePluginBFunctions.UnregisterLuaFunctions(LuaRuntime.AcquireState());
+ LuaStatus result = CoexistencePluginBFunctions.UnregisterLuaFunctions(LuaRuntime.AcquireState());
HostLog.Write(result.IsOk ? HostLogLevel.Information : HostLogLevel.Error,
"CheatEngine.SDK coexistence B: UnregisterLuaFunctions -> " + result + ".");
}
diff --git a/tests/CheatEngine.SDK.LivePlugin/CheatEngineSdkLivePlugin.cs b/tests/CheatEngine.SDK.LivePlugin/CheatEngineSdkLivePlugin.cs
index 9fe9f6e1..3feee226 100644
--- a/tests/CheatEngine.SDK.LivePlugin/CheatEngineSdkLivePlugin.cs
+++ b/tests/CheatEngine.SDK.LivePlugin/CheatEngineSdkLivePlugin.cs
@@ -6,6 +6,7 @@
using CheatEngine.SDK.Hosting.Context;
using CheatEngine.SDK.Hosting.Diagnostics;
using CheatEngine.SDK.Hosting.Plugin;
+using CheatEngine.SDK.Lua.Calls;
using CheatEngine.SDK.Lua.Runtime;
using CheatEngine.SDK.Lua.State;
@@ -33,7 +34,7 @@ protected override void OnEnable()
LogEnableDiagnostics(enableCount);
LuaState state = LuaRuntime.AcquireState();
- var registered = LiveFunctions.RegisterLuaFunctions(state);
+ LuaStatus registered = LiveFunctions.RegisterLuaFunctions(state);
HostLog.Write(
registered.IsOk ? HostLogLevel.Information : HostLogLevel.Error,
string.Create(CultureInfo.InvariantCulture,
@@ -46,7 +47,7 @@ protected override void OnEnable()
protected override void OnDisable()
{
LuaState state = LuaRuntime.AcquireState();
- var unregistered = LiveFunctions.UnregisterLuaFunctions(state);
+ LuaStatus unregistered = LiveFunctions.UnregisterLuaFunctions(state);
HostLog.Write(
unregistered.IsOk ? HostLogLevel.Information : HostLogLevel.Error,
string.Create(CultureInfo.InvariantCulture,
@@ -70,10 +71,9 @@ private static void LogEnableDiagnostics(int enableCount)
private static void ReadMemoryAdjacentPrimitive()
{
- // A placeholder address: point it at a readable location in an attached target (for example a module base
- // from getAddress()) to log a real value. Address supplies the number-or-hex-text convention and the
- // culture-invariant formatting. The read goes through this plugin's own readInteger binding (MemoryBindings);
- // CheatEngine.SDK.Engine.Generated.MemoryScalars.TryReadInt32 offers the same call as a ready-made wrapper.
+ // A placeholder address: point it at a readable location in an attached target to log a real value. Address
+ // supplies the number-or-hex-text convention and culture-invariant formatting. The read uses this plugin's
+ // generated integer binding; the SDK also exposes the same operation through its ready-made scalar wrapper.
Address probe = Address.FromUInt64(0x00400000UL);
bool ok = MemoryBindings.TryReadInt32((nuint) probe.Value, out int value);
HostLog.Write(
diff --git a/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs b/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs
index 9f500e56..eba8512b 100644
--- a/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs
+++ b/tests/CheatEngine.SDK.LiveProbe/LiveProbeAuthorization.cs
@@ -23,98 +23,152 @@ internal static class LiveProbeAuthorization
Justification = "Authorization evaluation keeps all fail-closed checks and diagnostics in one auditable sequence.")]
internal static AuthorizationDecision Evaluate()
{
+ if (!TryAuthorizeManifest(out AuthorizationManifest manifest, out string manifestFailure))
+ {
+ return AuthorizationDecision.Denied(manifestFailure);
+ }
+
+ if (!TryAuthorizeHost(manifest, out string hostPath, out string hostHash, out string hostFailure))
+ {
+ return AuthorizationDecision.Denied(hostFailure);
+ }
+
+ if (!TryAuthorizeTarget(manifest, out string targetPath, out string targetHash, out string targetFailure))
+ {
+ return AuthorizationDecision.Denied(targetFailure);
+ }
+
+ return AuthorizationDecision.Allowed(hostPath, hostHash, manifest.TargetProcessId, targetPath, targetHash,
+ manifest.ExpiresUtc);
+ }
+
+ private static bool TryAuthorizeManifest(out AuthorizationManifest manifest, out string failure)
+ {
+ manifest = default;
+ failure = string.Empty;
if (IntPtr.Size != 8)
{
- return AuthorizationDecision.Denied("The current process is not x64.");
+ failure = "The current process is not x64.";
+ return false;
}
string? acknowledgement = Environment.GetEnvironmentVariable(AcknowledgementVariable);
if (!string.Equals(acknowledgement, Acknowledgement, StringComparison.Ordinal))
{
- return AuthorizationDecision.Denied("The explicit CE_SDK_LIVE_PROBE_ACKNOWLEDGEMENT phrase is absent.");
+ failure = "The explicit CE_SDK_LIVE_PROBE_ACKNOWLEDGEMENT phrase is absent.";
+ return false;
}
string? manifestPath = Environment.GetEnvironmentVariable(ManifestVariable);
if (string.IsNullOrWhiteSpace(manifestPath))
{
- return AuthorizationDecision.Denied("CE_SDK_LIVE_PROBE_AUTHORIZATION_FILE is absent.");
+ failure = "CE_SDK_LIVE_PROBE_AUTHORIZATION_FILE is absent.";
+ return false;
}
- if (!TryReadManifest(manifestPath, out AuthorizationManifest manifest, out string manifestFailure))
+ if (!TryReadManifest(manifestPath, out manifest, out failure))
{
- return AuthorizationDecision.Denied(manifestFailure);
+ return false;
}
if (!string.Equals(manifest.Acknowledgement, Acknowledgement, StringComparison.Ordinal))
{
- return AuthorizationDecision.Denied("The authorization manifest has no matching acknowledgement.");
+ failure = "The authorization manifest has no matching acknowledgement.";
+ return false;
}
if (!manifest.Disposable)
{
- return AuthorizationDecision.Denied("The authorization manifest does not mark the target disposable.");
+ failure = "The authorization manifest does not mark the target disposable.";
+ return false;
}
if (manifest.ExpiresUtc <= DateTimeOffset.UtcNow)
{
- return AuthorizationDecision.Denied("The authorization manifest has expired.");
+ failure = "The authorization manifest has expired.";
+ return false;
}
- if (!TryGetProcessImage(Environment.ProcessId, out string hostPath, out string hostFailure))
+ return true;
+ }
+
+ private static bool TryAuthorizeHost(AuthorizationManifest manifest, out string hostPath, out string hostHash,
+ out string failure)
+ {
+ hostPath = string.Empty;
+ hostHash = string.Empty;
+ failure = string.Empty;
+ if (!TryGetProcessImage(Environment.ProcessId, out hostPath, out string hostFailure))
{
- return AuthorizationDecision.Denied("The CE host image cannot be inspected: " + hostFailure);
+ failure = "The CE host image cannot be inspected: " + hostFailure;
+ return false;
}
if (!IsAmd64Pe(hostPath, out string hostArchitectureFailure))
{
- return AuthorizationDecision.Denied("The CE host image is not an AMD64 PE: " + hostArchitectureFailure);
+ failure = "The CE host image is not an AMD64 PE: " + hostArchitectureFailure;
+ return false;
}
- if (!TryHash(hostPath, out string hostHash, out string hostHashFailure))
+ if (!TryHash(hostPath, out hostHash, out string hostHashFailure))
{
- return AuthorizationDecision.Denied("The CE host image cannot be hashed: " + hostHashFailure);
+ failure = "The CE host image cannot be hashed: " + hostHashFailure;
+ return false;
}
if (!string.Equals(hostHash, ExactCheatEngineSha256, StringComparison.Ordinal))
{
- return AuthorizationDecision.Denied("The host SHA-256 is not the pinned CE 7.7.0.10621 x64 binary.");
+ failure = "The host SHA-256 is not the pinned CE 7.7.0.10621 x64 binary.";
+ return false;
}
if (!string.Equals(manifest.HostSha256, ExactCheatEngineSha256, StringComparison.Ordinal))
{
- return AuthorizationDecision.Denied("The manifest does not pin the CE 7.7.0.10621 x64 SHA-256.");
+ failure = "The manifest does not pin the CE 7.7.0.10621 x64 SHA-256.";
+ return false;
}
string? hostVersion = FileVersionInfo.GetVersionInfo(hostPath).FileVersion;
if (!string.Equals(hostVersion, ExactCheatEngineFileVersion, StringComparison.Ordinal))
{
- return AuthorizationDecision.Denied("The pinned CE executable has an unexpected file version: " +
- hostVersion + ".");
+ failure = "The pinned CE executable has an unexpected file version: " + hostVersion + ".";
+ return false;
}
+ return true;
+ }
+
+ private static bool TryAuthorizeTarget(AuthorizationManifest manifest, out string targetPath, out string targetHash,
+ out string failure)
+ {
+ targetPath = string.Empty;
+ targetHash = string.Empty;
+ failure = string.Empty;
if (manifest.TargetProcessId == Environment.ProcessId)
{
- return AuthorizationDecision.Denied("The declared disposable target is the Cheat Engine host itself.");
+ failure = "The declared disposable target is the Cheat Engine host itself.";
+ return false;
}
- if (!TryGetProcessImage(manifest.TargetProcessId, out string targetPath, out string targetFailure))
+ if (!TryGetProcessImage(manifest.TargetProcessId, out targetPath, out string targetFailure))
{
- return AuthorizationDecision.Denied("The declared disposable target cannot be inspected: " + targetFailure);
+ failure = "The declared disposable target cannot be inspected: " + targetFailure;
+ return false;
}
- if (!TryHash(targetPath, out string targetHash, out string targetHashFailure))
+ if (!TryHash(targetPath, out targetHash, out string targetHashFailure))
{
- return AuthorizationDecision.Denied("The declared disposable target cannot be hashed: " +
- targetHashFailure);
+ failure = "The declared disposable target cannot be hashed: " + targetHashFailure;
+ return false;
}
if (!string.Equals(targetHash, manifest.TargetSha256, StringComparison.Ordinal))
{
- return AuthorizationDecision.Denied("The declared target SHA-256 differs from its live process image.");
+ failure = "The declared target SHA-256 differs from its live process image.";
+ return false;
}
- return AuthorizationDecision.Allowed(hostPath, hostHash, manifest.TargetProcessId, targetPath, targetHash,
- manifest.ExpiresUtc);
+ return true;
}
private static bool TryReadManifest(string path, out AuthorizationManifest manifest, out string failure)
diff --git a/tests/CheatEngine.SDK.Lua.Interop.Tests/RoundTrips/CallbackTests.cs b/tests/CheatEngine.SDK.Lua.Interop.Tests/RoundTrips/CallbackTests.cs
index e7048120..9cb13693 100644
--- a/tests/CheatEngine.SDK.Lua.Interop.Tests/RoundTrips/CallbackTests.cs
+++ b/tests/CheatEngine.SDK.Lua.Interop.Tests/RoundTrips/CallbackTests.cs
@@ -228,8 +228,8 @@ private static int Add(lua_State* L)
// Argument validation by inspection: a managed callback must not use luaL_check*, which raises.
int firstIsNumber;
int secondIsNumber;
- Double first = lua_tonumberx(L, 1, &firstIsNumber);
- Double second = lua_tonumberx(L, 2, &secondIsNumber);
+ double first = lua_tonumberx(L, 1, &firstIsNumber);
+ double second = lua_tonumberx(L, 2, &secondIsNumber);
if (firstIsNumber == 0 || secondIsNumber == 0)
{
lua_pushnil(L);
diff --git a/tests/CheatEngine.SDK.Lua.Tests/Callbacks/LuaCallbackTests.cs b/tests/CheatEngine.SDK.Lua.Tests/Callbacks/LuaCallbackTests.cs
index 462bf03c..07cd4881 100644
--- a/tests/CheatEngine.SDK.Lua.Tests/Callbacks/LuaCallbackTests.cs
+++ b/tests/CheatEngine.SDK.Lua.Tests/Callbacks/LuaCallbackTests.cs
@@ -353,14 +353,14 @@ public async Task Detach_closes_admission_and_drains_callback_creation_before_re
using ManualResetEventSlim admissionClosed = new(false);
Counter counter = new();
- LuaCallback.BeforeRegistryAddForTesting = () =>
+ LuaCallback.SetBeforeRegistryAddForTesting(() =>
{
creationPaused.Set();
if (!allowPublication.Wait(TimeSpan.FromSeconds(5), cancellationToken))
{
throw new TimeoutException("The callback-publication barrier timed out.");
}
- };
+ });
LuaRuntime.OperationAdmissionClosedForTesting = admissionClosed.Set;
try
@@ -394,7 +394,7 @@ public async Task Detach_closes_admission_and_drains_callback_creation_before_re
}
finally
{
- LuaCallback.BeforeRegistryAddForTesting = null;
+ LuaCallback.SetBeforeRegistryAddForTesting(null);
LuaRuntime.OperationAdmissionClosedForTesting = null;
allowPublication.Set();
}
@@ -781,7 +781,7 @@ public DisposeAdmissionFailureRace(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
LuaRuntime.OperationAdmissionClosedForTesting = OnAdmissionClosed;
- LuaCallback.DisposeAdmissionRefusedForTesting = OnDisposeAdmissionRefused;
+ LuaCallback.SetDisposeAdmissionRefusedForTesting(OnDisposeAdmissionRefused);
LuaCallbackRegistry.AfterReleaseForTesting = ThrowCleanupFailure;
}
@@ -819,7 +819,7 @@ public void Dispose()
public static void DisableFailureSeams()
{
LuaRuntime.OperationAdmissionClosedForTesting = null;
- LuaCallback.DisposeAdmissionRefusedForTesting = null;
+ LuaCallback.SetDisposeAdmissionRefusedForTesting(null);
LuaCallbackRegistry.AfterReleaseForTesting = null;
}
diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.csproj b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.csproj
index 84d5c59b..fcd78339 100644
--- a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.csproj
+++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.csproj
@@ -13,6 +13,9 @@
+
+
diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/NativeBridgeParityTests.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/NativeBridgeParityTests.cs
new file mode 100644
index 00000000..e3e72ed7
--- /dev/null
+++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Generator/NativeBridgeParityTests.cs
@@ -0,0 +1,113 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+using CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.Infrastructure;
+
+namespace CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.Generator;
+
+/// Parity tests for the production protected-operation catalogue and its C11 implementation.
+public sealed class NativeBridgeParityTests
+{
+ private static readonly TimeSpan RegularExpressionTimeout = TimeSpan.FromSeconds(1);
+
+ private static readonly NativeOperation[] ExpectedOperations =
+ [
+ new("OP_PUSH_BYTES", 0),
+ new("OP_CREATE_TABLE", 1),
+ new("OP_NEW_USERDATA", 2),
+ new("OP_PUSH_CLOSURE", 3),
+ new("OP_RAWSET", 4),
+ new("OP_RAWSETI", 5),
+ new("OP_RAWSETP", 6),
+ new("OP_REF", 7),
+ new("OP_PUSH_REF", 8),
+ new("OP_UNREF", 9),
+ new("OP_PUSH_HOST_OBJECT", 10),
+ new("OP_PUSH_BYTE_TABLE", 11)
+ ];
+
+ [Fact]
+ public void Production_catalogue_C11_enum_switch_and_operation_mask_remain_in_lockstep()
+ {
+ string source = ProductionNativeBridge.Read();
+ string nativeEnum = ExtractNativeOperationEnum(source);
+ string operationSwitch = ExtractOperationSwitch(source);
+ string operationMask = ExtractOperationMask(source);
+
+ Assert.Equal(ExpectedOperations.Length, CountOperationEnumValues(nativeEnum));
+ Assert.Equal(ExpectedOperations.Length, ExtractOperationCount(nativeEnum));
+
+ for (int i = 0; i < ExpectedOperations.Length; i++)
+ {
+ NativeOperation operation = ExpectedOperations[i];
+ Assert.True(HasNativeEnumValue(nativeEnum, operation),
+ $"The native operation enum does not define {operation.Name} = {operation.Opcode}.");
+ Assert.True(HasOperationCase(operationSwitch, operation.Name),
+ $"The native protected-operation switch has no case for {operation.Name}.");
+ Assert.Contains(operation.Name, operationMask, StringComparison.Ordinal);
+ }
+ }
+
+ private static string ExtractNativeOperationEnum(string source)
+ {
+ Match match = Regex.Match(source,
+ "enum\\s*\\{\\s*(?OP_PUSH_BYTES\\s*=\\s*0,[\\s\\S]*?CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT\\s*=\\s*\\d+)\\s*\\};",
+ RegexOptions.CultureInvariant,
+ RegularExpressionTimeout);
+ Assert.True(match.Success, "The C11 protected-operation enum was not found.");
+ return match.Groups["values"].Value;
+ }
+
+ private static string ExtractOperationSwitch(string source)
+ {
+ Match match = Regex.Match(source,
+ "switch\\s*\\(c->op\\)\\s*\\{(?[\\s\\S]*?)\\n\\s*default:",
+ RegexOptions.CultureInvariant,
+ RegularExpressionTimeout);
+ Assert.True(match.Success, "The C11 protected-operation switch was not found.");
+ return match.Groups["cases"].Value;
+ }
+
+ private static string ExtractOperationMask(string source)
+ {
+ Match match = Regex.Match(source,
+ "#define\\s+CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_MASK\\s*\\\\(?[\\s\\S]*?)\\n\\s*\\n",
+ RegexOptions.CultureInvariant,
+ RegularExpressionTimeout);
+ Assert.True(match.Success, "The C11 protected-operation mask was not found.");
+ return match.Groups["mask"].Value;
+ }
+
+ private static int CountOperationEnumValues(string nativeEnum)
+ {
+ MatchCollection matches = Regex.Matches(nativeEnum,
+ "\\bOP_[A-Z_]+\\s*=\\s*\\d+",
+ RegexOptions.CultureInvariant,
+ RegularExpressionTimeout);
+ return matches.Count;
+ }
+
+ private static int ExtractOperationCount(string nativeEnum)
+ {
+ Match match = Regex.Match(nativeEnum,
+ "CHEATENGINE_SDK_LUA_BRIDGE_OPERATION_COUNT\\s*=\\s*(?\\d+)",
+ RegexOptions.CultureInvariant,
+ RegularExpressionTimeout);
+ Assert.True(match.Success, "The C11 protected-operation count sentinel was not found.");
+ return int.Parse(match.Groups["count"].Value, CultureInfo.InvariantCulture);
+ }
+
+ private static bool HasNativeEnumValue(string nativeEnum, NativeOperation operation)
+ {
+ string pattern = $"\\b{Regex.Escape(operation.Name)}\\s*=\\s*{operation.Opcode}\\b";
+ return Regex.IsMatch(nativeEnum, pattern, RegexOptions.CultureInvariant, RegularExpressionTimeout);
+ }
+
+ private static bool HasOperationCase(string operationSwitch, string operationName)
+ {
+ string pattern = $"\\bcase\\s+{Regex.Escape(operationName)}\\s*:";
+ return Regex.IsMatch(operationSwitch, pattern, RegexOptions.CultureInvariant, RegularExpressionTimeout);
+ }
+
+ private readonly record struct NativeOperation(string Name, int Opcode);
+}
diff --git a/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Infrastructure/ProductionNativeBridge.cs b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Infrastructure/ProductionNativeBridge.cs
new file mode 100644
index 00000000..a0a9c126
--- /dev/null
+++ b/tests/CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests/Infrastructure/ProductionNativeBridge.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Text;
+
+namespace CheatEngine.SDK.SourceGenerators.LuaBridgeContract.Tests.Infrastructure;
+
+/// The exact checked-in C11 bridge source embedded as deterministic parity-test data.
+internal static class ProductionNativeBridge
+{
+ private const string ResourceName = "CheatEngine.SDK.LuaBridgeContract.Tests.ProductionNativeBridge.c";
+
+ public static string Read()
+ {
+ Assembly assembly = typeof(ProductionNativeBridge).Assembly;
+ using Stream? stream = assembly.GetManifestResourceStream(ResourceName);
+ Assert.NotNull(stream);
+ using StreamReader reader = new(stream, Encoding.UTF8, true);
+ return reader.ReadToEnd();
+ }
+}
diff --git a/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs b/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs
index cb235401..012779fc 100644
--- a/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs
+++ b/tests/CheatEngine.SDK.Tests/Infrastructure/PackagedUmbrellaFixture.cs
@@ -269,6 +269,34 @@ public string PackedAotConsumerPublishOutput
private set;
} = "";
+ ///
+ /// The AOT-relevant diagnostics configured as errors by the package-only consumer before it is published.
+ ///
+ public string PackedAotConsumerWarningsAsErrors
+ {
+ get;
+ private set;
+ } = "";
+
+ ///
+ /// Whether the package-only AOT consumer explicitly enabled the unsafe generated Lua thunks require.
+ ///
+ public string PackedAotConsumerAllowUnsafeBlocks
+ {
+ get;
+ private set;
+ } = "";
+
+ ///
+ /// The evaluated Native AOT publication settings and diagnostic suppressions for the package-only consumer.
+ ///
+ public IReadOnlyDictionary PackedAotConsumerProperties
+ {
+ get;
+ private set;
+ } =
+ new Dictionary(StringComparer.Ordinal);
+
///
/// Whether the published package-only trim and Native AOT executable completed successfully.
///
@@ -379,9 +407,12 @@ public ValueTask DisposeAsync()
catch (IOException)
{
// Best effort: a file a virus scanner or editor still has open must not fail the test run.
+ _tempRoot = null;
}
catch (UnauthorizedAccessException)
{
+ // Best effort: an external process may temporarily deny the recursive cleanup operation.
+ _tempRoot = null;
}
}
@@ -437,8 +468,13 @@ private async Task InitializeDefaultConsumerAsync(string tempRoot, string feedDi
{
ThrowawayConsumer consumer = ThrowawayConsumer.Create(tempRoot, "DefaultConsumer", PackageVersion,
feedDirectory,
- includeLegacyAobConsumer: true, includeTargetBoundAllocationConsumer: true,
- includeRecordAndSymbolContract: true, includeValueScanConsumer: true);
+ new ThrowawayConsumer.CreateOptions
+ {
+ IncludeLegacyAobConsumer = true,
+ IncludeTargetBoundAllocationConsumer = true,
+ IncludeRecordAndSymbolContract = true,
+ IncludeValueScanConsumer = true
+ });
await RestoreAndBuildAsync(consumer, packagesDirectory).ConfigureAwait(false);
LegacyAobConsumerBuildSucceeded = File.Exists(Path.Combine(consumer.Directory, "LegacyAobConsumer.cs"));
DefaultProperties = await consumer.GetPropertiesAsync(BuildTimeout, "AllowUnsafeBlocks", "EnableDynamicLoading",
@@ -462,7 +498,10 @@ private async Task InitializeExplicitUnsafeFalseConsumerAsync(string tempRoot, s
{
ThrowawayConsumer consumer = ThrowawayConsumer.Create(tempRoot, "ExplicitUnsafeFalseConsumer", PackageVersion,
feedDirectory,
- " false\n");
+ new ThrowawayConsumer.CreateOptions
+ {
+ ExtraProperties = " false\n"
+ });
await RestoreAndBuildAsync(consumer, packagesDirectory).ConfigureAwait(false);
ExplicitUnsafeFalseProperties = await consumer.GetPropertiesAsync(BuildTimeout, "AllowUnsafeBlocks")
.ConfigureAwait(false);
@@ -473,7 +512,10 @@ private async Task InitializeEntryPointOffConsumerAsync(string tempRoot, string
{
ThrowawayConsumer consumer = ThrowawayConsumer.Create(tempRoot, "EntryPointOffConsumer", PackageVersion,
feedDirectory,
- " false\n");
+ new ThrowawayConsumer.CreateOptions
+ {
+ ExtraProperties = " false\n"
+ });
// CESDK0003 deliberately makes the handoff explicit: disabling generation transfers ownership of the exact
// host lookup identity to the plugin author. If the generator ignored the false switch, this source would also
// make the consumer fail with the duplicate CESDK.CESDK type - so a successful build proves both contracts.
@@ -494,7 +536,10 @@ private async Task InitializeLuaFunctionConsumersAsync(string tempRoot, string f
{
ThrowawayConsumer optInConsumer = ThrowawayConsumer.Create(tempRoot, "LuaFunctionOptInConsumer", PackageVersion,
feedDirectory,
- " true\n", includeLuaFunction: true);
+ new ThrowawayConsumer.CreateOptions
+ {
+ ExtraProperties = " true\n", IncludeLuaFunction = true
+ });
ProcessResult optInRestore =
await optInConsumer.RestoreAsync(RestoreTimeout, packagesDirectory).ConfigureAwait(false);
EnsureSucceeded(optInRestore, "dotnet restore", optInConsumer.ProjectPath);
@@ -504,7 +549,8 @@ private async Task InitializeLuaFunctionConsumersAsync(string tempRoot, string f
ThrowawayConsumer withoutUnsafeConsumer = ThrowawayConsumer.Create(tempRoot, "LuaFunctionWithoutUnsafeConsumer",
PackageVersion,
- feedDirectory, includeLuaFunction: true);
+ feedDirectory,
+ new ThrowawayConsumer.CreateOptions { IncludeLuaFunction = true });
ProcessResult restore = await withoutUnsafeConsumer.RestoreAsync(RestoreTimeout, packagesDirectory)
.ConfigureAwait(false);
EnsureSucceeded(restore, "dotnet restore", withoutUnsafeConsumer.ProjectPath);
@@ -557,15 +603,23 @@ private async Task InitializePackedAotConsumerAsync(string tempRoot, string feed
ThrowawayConsumer consumer = ThrowawayConsumer.CreateAotExecutable(tempRoot, consumerName, PackageVersion,
feedDirectory,
"""
+ true
win-x64
true
true
true
true
- IL3058
+ IL2026;IL3050;IL3058
""");
ProcessResult restore = await consumer.RestoreAsync(RestoreTimeout, packagesDirectory).ConfigureAwait(false);
EnsureSucceeded(restore, "dotnet restore", consumer.ProjectPath);
+ IReadOnlyDictionary aotProperties = await consumer
+ .GetPropertiesAsync(BuildTimeout, "AllowUnsafeBlocks", "PublishAot", "PublishTrimmed", "SelfContained",
+ "RuntimeIdentifier", "VerifyReferenceAotCompatibility", "WarningsAsErrors", "NoWarn")
+ .ConfigureAwait(false);
+ PackedAotConsumerProperties = aotProperties;
+ PackedAotConsumerAllowUnsafeBlocks = aotProperties["AllowUnsafeBlocks"];
+ PackedAotConsumerWarningsAsErrors = aotProperties["WarningsAsErrors"];
ProcessResult build = await consumer.BuildAsync(BuildTimeout).ConfigureAwait(false);
EnsureSucceeded(build, "dotnet build", consumer.ProjectPath);
@@ -578,7 +632,15 @@ private async Task InitializePackedAotConsumerAsync(string tempRoot, string feed
return;
}
- ProcessResult run = await consumer.RunPublishedAsync(publishDirectory, RuntimeRunTimeout).ConfigureAwait(false);
+ string bundledLuaPath = RepositoryLayout.PathOf("native/cheat-engine/lua53-64.dll");
+ if (!File.Exists(bundledLuaPath))
+ {
+ throw new InvalidOperationException(
+ $"The package-only AOT consumer requires the bundled test-only Lua fixture at '{bundledLuaPath}'.");
+ }
+
+ ProcessResult run = await consumer.RunPublishedAsync(publishDirectory, bundledLuaPath, RuntimeRunTimeout)
+ .ConfigureAwait(false);
PackedAotConsumerRunSucceeded = run.ExitCode == 0;
PackedAotConsumerRunOutput = run.CombinedOutput;
}
@@ -613,7 +675,7 @@ private async Task InitializePlatformTargetConsumersAsync(string tempRoot, strin
foreach ((string key, string consumerName, string? platformTarget) in PlatformTargetConsumers)
{
ThrowawayConsumer consumer = ThrowawayConsumer.Create(tempRoot, consumerName, PackageVersion, feedDirectory,
- platformTarget: platformTarget);
+ new ThrowawayConsumer.CreateOptions { PlatformTarget = platformTarget });
ProcessResult restore =
await consumer.RestoreAsync(RestoreTimeout, packagesDirectory).ConfigureAwait(false);
EnsureSucceeded(restore, "dotnet restore", consumer.ProjectPath);
diff --git a/tests/CheatEngine.SDK.Tests/Infrastructure/ThrowawayConsumer.cs b/tests/CheatEngine.SDK.Tests/Infrastructure/ThrowawayConsumer.cs
index 98a911a3..72c4e81f 100644
--- a/tests/CheatEngine.SDK.Tests/Infrastructure/ThrowawayConsumer.cs
+++ b/tests/CheatEngine.SDK.Tests/Infrastructure/ThrowawayConsumer.cs
@@ -261,8 +261,13 @@ private static long ExecuteForInteger(LuaState state, ReadOnlySpan source)
private const string AotProgramSource = """
using System;
+ using System.Runtime.CompilerServices;
+ using System.Runtime.InteropServices;
using CheatEngine.SDK.Annotations.Lua;
using CheatEngine.SDK.Lua.Calls;
+ using CheatEngine.SDK.Lua.Interop.Api;
+ using CheatEngine.SDK.Lua.Interop.Types;
+ using CheatEngine.SDK.Lua.Runtime;
using CheatEngine.SDK.Lua.State;
namespace ThrowawayAot;
@@ -273,18 +278,87 @@ internal static partial class GeneratedAotBinding
public static partial LuaOperationStatus TryProbe(LuaState state);
}
- internal static class Program
+ // This is a standalone native-Lua probe. It uses the test fixture's Lua 5.3 DLL,
+ // not a Cheat Engine host, and executes the generated binding before reporting success.
+ internal static unsafe class Program
{
+ private static nint s_state;
+
public static int Main(string[] args)
{
- // Native AOT keeps this generated body because the non-default command-line
- // path references it, while the normal no-host test run never invokes it.
- if (args.Length != 0)
- _ = GeneratedAotBinding.TryProbe(default);
-
- Console.WriteLine("SDK-022-AOT-STANDALONE");
- Console.WriteLine("SDK-022-AOT-NO-CE-HOST");
- return 0;
+ if (args.Length != 1)
+ {
+ Console.Error.WriteLine("Expected exactly one Lua 5.3 DLL path.");
+ return 64;
+ }
+
+ try
+ {
+ Run(args[0]);
+ Console.WriteLine("SDK-022-AOT-GENERATED-BINDING");
+ Console.WriteLine("SDK-022-AOT-STANDALONE");
+ Console.WriteLine("SDK-022-AOT-NO-CE-HOST");
+ return 0;
+ }
+ catch (Exception exception)
+ {
+ Console.Error.WriteLine(exception);
+ return 1;
+ }
+ }
+
+ private static void Run(string luaLibraryPath)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(luaLibraryPath);
+ LuaApi.Initialize(NativeLibrary.Load(luaLibraryPath));
+ lua_State* statePointer = LuaApi.luaL_newstate();
+ if (statePointer is null)
+ throw new InvalidOperationException("The Lua fixture could not create a state.");
+
+ try
+ {
+ LuaApi.luaL_openlibs(statePointer);
+ LuaState state = new((nint)statePointer);
+ s_state = state.Handle;
+ delegate* unmanaged[Stdcall] stateProvider = &ProvideState;
+ LuaHostBinding binding = new((nint)stateProvider, 0,
+ Environment.CurrentManagedThreadId);
+ LuaRuntime.Attach(in binding);
+ try
+ {
+ using LuaFrame frame = new(state);
+ LuaStatus setup = state.TryExecute(
+ "sdk022_aot_probe_invocations = 0; function sdk022_aot_probe() sdk022_aot_probe_invocations = sdk022_aot_probe_invocations + 1 end"u8,
+ 0, "=sdk022-aot"u8);
+ if (!setup.IsOk)
+ throw new InvalidOperationException("The Lua fixture setup failed with " + setup + ".");
+
+ LuaOperationStatus probe = GeneratedAotBinding.TryProbe(state);
+ if (!probe.IsSuccess)
+ throw new InvalidOperationException("The generated AOT binding failed with " + probe.Kind + ".");
+
+ LuaStatus invocationCount = state.TryExecute(
+ "assert(sdk022_aot_probe_invocations == 1, 'GeneratedAotBinding.TryProbe did not invoke the Lua probe exactly once.')"u8,
+ 0, "=sdk022-aot-assertion"u8);
+ if (!invocationCount.IsOk)
+ throw new InvalidOperationException("The generated AOT binding invocation assertion failed with " + invocationCount + ".");
+ }
+ finally
+ {
+ LuaRuntime.Detach();
+ s_state = 0;
+ }
+ }
+ finally
+ {
+ LuaApi.lua_close(statePointer);
+ }
+ }
+
+ [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
+ private static void* ProvideState()
+ {
+ return (void*)s_state;
}
}
""";
@@ -471,40 +545,38 @@ public string AssemblyPath
/// Scaffolds a project named under : an
/// net10.0 class library with one PackageReference to CheatEngine.SDK. Its generated
/// NuGet.Config maps that exact package identity to while retaining
- /// nuget.org for other package identities. The project has one minimal but valid plugin class and whatever
- /// adds to its single PropertyGroup. When
- /// is ,
- /// the project also declares one valid [LuaFunction] export. When
- /// is , it compiles both historical AobScanner.TryScan overloads against the packed
- /// SDK. When is , it compiles an
+ /// nuget.org for other package identities. The project has one minimal but valid plugin class and the optional
+ /// additions to its single PropertyGroup. When
+ /// is , the project also declares one
+ /// valid [LuaFunction] export. When is
+ /// , it compiles both historical AobScanner.TryScan overloads against the packed
+ /// SDK. When is , it compiles
+ /// an
/// independent implementation of the target-bound allocation backend seam against that package. When
- /// is , it compiles the
+ /// is , it compiles the
/// SDK-021 typed mutation and coordinated-symbol surfaces without accessing raw CE handles.
- /// When is , it compiles the value-scan
- /// factory and bounded-copy APIs against the same packed SDK.
- /// defaults to x64, but may be to prove the package
- /// behavior when the consumer does not declare it.
+ /// When is , it compiles the
+ /// value-scan factory and bounded-copy APIs against the same packed SDK. The
+ /// property defaults to x64 and may be .
///
public static ThrowawayConsumer Create(string parentDirectory, string name, string cheatEngineSdkVersion,
- string localFeedDirectory, string extraProperties = "", string? platformTarget = "x64",
- bool includeLuaFunction = false, bool includeLegacyAobConsumer = false,
- bool includeTargetBoundAllocationConsumer = false, bool includeRecordAndSymbolContract = false,
- bool includeValueScanConsumer = false)
+ string localFeedDirectory, CreateOptions? options = null)
{
+ options ??= new CreateOptions();
string directory = Path.Combine(parentDirectory, name);
System.IO.Directory.CreateDirectory(directory);
string projectPath = Path.Combine(directory, $"{name}.csproj");
- string platformTargetProperty = platformTarget is null
+ string platformTargetProperty = options.PlatformTarget is null
? ""
- : $" {platformTarget}\n";
+ : $" {options.PlatformTarget}\n";
File.WriteAllText(projectPath, $"""
net10.0
{platformTargetProperty} enable
- {extraProperties}
+ {options.ExtraProperties}
@@ -512,28 +584,28 @@ public static ThrowawayConsumer Create(string parentDirectory, string name, stri
""");
File.WriteAllText(Path.Combine(directory, "Plugin.cs"), PluginSource);
- if (includeLuaFunction)
+ if (options.IncludeLuaFunction)
{
File.WriteAllText(Path.Combine(directory, "Functions.cs"), LuaFunctionSource);
}
- if (includeLegacyAobConsumer)
+ if (options.IncludeLegacyAobConsumer)
{
File.WriteAllText(Path.Combine(directory, "LegacyAobConsumer.cs"), LegacyAobSource);
}
- if (includeTargetBoundAllocationConsumer)
+ if (options.IncludeTargetBoundAllocationConsumer)
{
File.WriteAllText(Path.Combine(directory, "TargetBoundAllocationBackend.cs"), TargetBoundAllocationSource);
}
- if (includeRecordAndSymbolContract)
+ if (options.IncludeRecordAndSymbolContract)
{
File.WriteAllText(Path.Combine(directory, "RecordAndSymbolContractConsumer.cs"),
RecordAndSymbolContractSource);
}
- if (includeValueScanConsumer)
+ if (options.IncludeValueScanConsumer)
{
File.WriteAllText(Path.Combine(directory, "ValueScanConsumer.cs"), ValueScanSource);
}
@@ -569,8 +641,8 @@ public static ThrowawayConsumer CreateInvalidDuplicateLuaFunctionConsumer(string
}
///
- /// Scaffolds a package-only executable whose source includes a generated Lua binding but whose program neither
- /// loads a native Lua module nor activates a Cheat Engine host. The fixture supplies trim/AOT/RID properties.
+ /// Scaffolds a package-only executable whose source executes a generated Lua binding against the fixture-supplied
+ /// native Lua module. It never activates a Cheat Engine host; the fixture supplies trim/AOT/RID properties.
///
public static ThrowawayConsumer CreateAotExecutable(string parentDirectory, string name,
string cheatEngineSdkVersion,
@@ -718,13 +790,17 @@ public Task PublishAsync(TimeSpan timeout, string outputDirectory
timeout);
}
- /// Runs the native executable emitted by a publish into .
- public Task RunPublishedAsync(string outputDirectory, TimeSpan timeout)
+ ///
+ /// Runs the native executable emitted by a publish into with the Lua 5.3
+ /// fixture path the AOT probe requires to execute its generated binding.
+ ///
+ public Task RunPublishedAsync(string outputDirectory, string luaLibraryPath, TimeSpan timeout)
{
ArgumentException.ThrowIfNullOrWhiteSpace(outputDirectory);
+ ArgumentException.ThrowIfNullOrWhiteSpace(luaLibraryPath);
string executableName = Path.GetFileNameWithoutExtension(ProjectPath) + ".exe";
string executablePath = Path.Combine(outputDirectory, executableName);
- return ProcessRunner.RunAsync(executablePath, "", outputDirectory, timeout);
+ return ProcessRunner.RunAsync(executablePath, $"\"{luaLibraryPath}\"", outputDirectory, timeout);
}
///
@@ -747,8 +823,7 @@ public async Task> GetPropertiesAsync(TimeSp
Dictionary values = new(StringComparer.Ordinal);
- // MSBuild's own documented split: "-getProperty to request a single property" emits a bare string;
- // several properties (this project always requests at least one, so >= 2 here) emit one JSON object.
+ // MSBuild emits a bare value for one requested property and a properties object when several are requested.
if (propertyNames.Length == 1)
{
values[propertyNames[0]] = result.StandardOutput.Trim();
@@ -773,4 +848,50 @@ public async Task> GetPropertiesAsync(TimeSp
return values;
}
+
+ /// Optional source and project switches used when scaffolding a packaged consumer.
+ internal sealed class CreateOptions
+ {
+ public string ExtraProperties
+ {
+ get;
+ init;
+ } = "";
+
+ public string? PlatformTarget
+ {
+ get;
+ init;
+ } = "x64";
+
+ public bool IncludeLuaFunction
+ {
+ get;
+ init;
+ }
+
+ public bool IncludeLegacyAobConsumer
+ {
+ get;
+ init;
+ }
+
+ public bool IncludeTargetBoundAllocationConsumer
+ {
+ get;
+ init;
+ }
+
+ public bool IncludeRecordAndSymbolContract
+ {
+ get;
+ init;
+ }
+
+ public bool IncludeValueScanConsumer
+ {
+ get;
+ init;
+ }
+ }
}
diff --git a/tests/CheatEngine.SDK.Tests/Packaging/NativeBridgePeAuditTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/NativeBridgePeAuditTests.cs
index a0921d2d..88ae1000 100644
--- a/tests/CheatEngine.SDK.Tests/Packaging/NativeBridgePeAuditTests.cs
+++ b/tests/CheatEngine.SDK.Tests/Packaging/NativeBridgePeAuditTests.cs
@@ -15,9 +15,10 @@ public sealed class NativeBridgePeAuditTests
private const string BridgeRelativePath =
"native/cheatengine-sdk-lua-bridge/runtimes/win-x64/native/cheatengine-sdk-lua-bridge.dll";
- private const string ManifestRelativePath = "native/cheatengine-sdk-lua-bridge/bridge-audit-manifest.json";
private const string SourceRelativePath = "native/cheatengine-sdk-lua-bridge/cheatengine_sdk_lua_bridge.c";
private const string BuildRelativePath = "native/cheatengine-sdk-lua-bridge/xmake.lua";
+ private const string ContinuousIntegrationWorkflowRelativePath = ".github/workflows/ci.yml";
+ private const string PinnedXmakeVersion = "3.0.9";
private static readonly string[] ExpectedExports =
[
@@ -75,52 +76,61 @@ public void Checked_in_bridge_imports_only_kernel32_without_delay_load()
}
[Fact]
- public void Audit_manifest_records_the_pinned_toolchain_flags_and_hashes()
+ public void Checked_in_bridge_exports_the_current_source_and_xmake_fingerprint()
{
- using JsonDocument manifest = ReadManifest();
- JsonElement root = manifest.RootElement;
-
- Assert.Equal(2, root.GetProperty("schemaVersion").GetInt32());
- Assert.Equal("xmake", root.GetProperty("toolchain").GetProperty("buildSystem").GetProperty("name").GetString());
- Assert.Equal("3.0.9",
- root.GetProperty("toolchain").GetProperty("buildSystem").GetProperty("version").GetString());
- Assert.Equal("MSVC", root.GetProperty("toolchain").GetProperty("compiler").GetProperty("name").GetString());
- Assert.Equal("C11", root.GetProperty("toolchain").GetProperty("languageStandard").GetString());
- Assert.Equal("MT", root.GetProperty("toolchain").GetProperty("cRuntime").GetString());
- AssertJsonStringSet(root.GetProperty("toolchain").GetProperty("compilerFlags"), ["/MT", "allextra", "error"]);
- AssertJsonStringSet(root.GetProperty("toolchain").GetProperty("linkerFlags"), ["/Brepro"]);
- Assert.True(root.GetProperty("reproducibility").GetProperty("doubleBuildSha256Comparison").GetBoolean());
-
- JsonElement nativeAsset = root.GetProperty("nativeAsset");
- Assert.Equal("PE32+", nativeAsset.GetProperty("pe").GetProperty("format").GetString());
- Assert.Equal("AMD64", nativeAsset.GetProperty("pe").GetProperty("machine").GetString());
- Assert.True(nativeAsset.GetProperty("pe").GetProperty("isDll").GetBoolean());
- AssertJsonStringSet(nativeAsset.GetProperty("exports"), ExpectedExports);
- AssertJsonStringSet(nativeAsset.GetProperty("delayImports"), []);
- AssertJsonStringSet(nativeAsset.GetProperty("imports"), s_allowedImportModules);
-
string sourceHash = CalculateSha256(RepositoryLayout.PathOf(SourceRelativePath));
string buildHash = CalculateSha256(RepositoryLayout.PathOf(BuildRelativePath));
string expectedFingerprint = $"{sourceHash}:{buildHash}";
- Assert.Equal(sourceHash,
- root.GetProperty("source").GetProperty("hashes").GetProperty("cheatengine_sdk_lua_bridge.c").GetString());
- Assert.Equal(buildHash, root.GetProperty("source").GetProperty("hashes").GetProperty("xmake.lua").GetString());
- Assert.Equal(expectedFingerprint, root.GetProperty("source").GetProperty("fingerprint").GetString());
-
- string bridgeHash = CalculateSha256(BridgePath);
- Assert.Equal(bridgeHash, nativeAsset.GetProperty("sha256").GetString());
Assert.Equal(expectedFingerprint,
ReadBridge().ReadExportedAsciiZ("cheatengine_sdk_lua_bridge_source_fingerprint"));
}
- private static PortableExecutableInspector ReadBridge()
+ [Fact]
+ public void Native_bridge_xmake_configuration_pins_the_required_compilation_contract()
{
- return PortableExecutableInspector.Read(BridgePath);
+ string xmakeConfiguration = ReadRepositoryText(BuildRelativePath);
+
+ Assert.Contains("set_languages(\"c11\")", xmakeConfiguration, StringComparison.Ordinal);
+ Assert.Contains("set_warnings(\"allextra\", \"error\")", xmakeConfiguration, StringComparison.Ordinal);
+ Assert.Contains("set_toolchains(\"msvc\")", xmakeConfiguration, StringComparison.Ordinal);
+ Assert.Contains("set_policy(\"build.c++.msvc.runtime\", \"MT\")", xmakeConfiguration,
+ StringComparison.Ordinal);
+ Assert.Contains("set_runtimes(\"MT\")", xmakeConfiguration, StringComparison.Ordinal);
+ Assert.Contains("add_cflags(\"/MT\", {tools = \"cl\", force = true})", xmakeConfiguration,
+ StringComparison.Ordinal);
+ Assert.Contains("add_shflags(\"/Brepro\", {tools = \"link\", force = true})", xmakeConfiguration,
+ StringComparison.Ordinal);
}
- private static JsonDocument ReadManifest()
+ [Fact]
+ public void Native_bridge_ci_pins_xmake_and_enforces_a_double_build_reproducibility_gate()
{
- return JsonDocument.Parse(File.ReadAllText(RepositoryLayout.PathOf(ManifestRelativePath)));
+ string workflow = ReadRepositoryText(ContinuousIntegrationWorkflowRelativePath);
+
+ Assert.Contains("xmake-io/github-action-setup-xmake@", workflow, StringComparison.Ordinal);
+ Assert.Contains($"xmake-version: '{PinnedXmakeVersion}'", workflow, StringComparison.Ordinal);
+ Assert.Contains("$primaryOutput = 'artifacts/native/cheatengine-sdk-lua-bridge'", workflow,
+ StringComparison.Ordinal);
+ Assert.Contains("$reproducibilityOutput = 'artifacts/native/cheatengine-sdk-lua-bridge-repro'", workflow,
+ StringComparison.Ordinal);
+ Assert.Contains("The primary and reproducibility bridge output directories must be distinct.", workflow,
+ StringComparison.Ordinal);
+ Assert.Contains("xmake f -P native/cheatengine-sdk-lua-bridge -o $primaryOutput", workflow,
+ StringComparison.Ordinal);
+ Assert.Contains("xmake f -P native/cheatengine-sdk-lua-bridge -o $reproducibilityOutput", workflow,
+ StringComparison.Ordinal);
+ Assert.Contains("$primaryHash = (Get-FileHash -LiteralPath $primaryBridge -Algorithm SHA256).Hash", workflow,
+ StringComparison.Ordinal);
+ Assert.Contains(
+ "$reproducibilityHash = (Get-FileHash -LiteralPath $reproducibilityBridge -Algorithm SHA256).Hash",
+ workflow, StringComparison.Ordinal);
+ Assert.Contains("$primaryHash, $reproducibilityHash, [StringComparison]::OrdinalIgnoreCase", workflow,
+ StringComparison.Ordinal);
+ }
+
+ private static PortableExecutableInspector ReadBridge()
+ {
+ return PortableExecutableInspector.Read(BridgePath);
}
private static List GetExportNames(PortableExecutableInspector image)
@@ -146,21 +156,9 @@ private static void AssertExactSet(string[] expected, List actual)
}
}
- private static void AssertJsonStringSet(JsonElement array, string[] expected)
+ private static string ReadRepositoryText(string relativePath)
{
- Assert.Equal(JsonValueKind.Array, array.ValueKind);
- Assert.Equal(expected.Length, array.GetArrayLength());
- HashSet values = new(StringComparer.Ordinal);
- foreach (JsonElement entry in array.EnumerateArray())
- {
- values.Add(entry.GetString() ?? string.Empty);
- }
-
- Assert.Equal(expected.Length, values.Count);
- for (int index = 0; index < expected.Length; index++)
- {
- Assert.Contains(expected[index], values, StringComparer.Ordinal);
- }
+ return File.ReadAllText(RepositoryLayout.PathOf(relativePath));
}
private static string CalculateSha256(string path)
diff --git a/tests/CheatEngine.SDK.Tests/Packaging/PackedConsumerBindingTests.cs b/tests/CheatEngine.SDK.Tests/Packaging/PackedConsumerBindingTests.cs
index b3f9a541..3bc108fe 100644
--- a/tests/CheatEngine.SDK.Tests/Packaging/PackedConsumerBindingTests.cs
+++ b/tests/CheatEngine.SDK.Tests/Packaging/PackedConsumerBindingTests.cs
@@ -9,6 +9,8 @@ namespace CheatEngine.SDK.Tests.Packaging;
[Collection(PackagedUmbrellaSuite.Name)]
public sealed class PackedConsumerBindingTests(PackagedUmbrellaFixture fixture)
{
+ private static readonly string[] RequiredAotWarningIds = ["IL2026", "IL3050", "IL3058"];
+
private static readonly string[] ExpectedLibNet10Assets =
[
"lib/net10.0/CheatEngine.SDK.dll",
@@ -69,10 +71,30 @@ public void Packaged_AOT_consumer_publishes_and_runs_only_as_a_standalone_execut
Assert.True(fixture.PackedAotConsumerRunSucceeded,
$"The published package-only AOT consumer did not exit successfully:{Environment.NewLine}" +
fixture.PackedAotConsumerRunOutput);
+ Assert.Contains("SDK-022-AOT-GENERATED-BINDING", fixture.PackedAotConsumerRunOutput,
+ StringComparison.Ordinal);
Assert.Contains("SDK-022-AOT-STANDALONE", fixture.PackedAotConsumerRunOutput, StringComparison.Ordinal);
Assert.Contains("SDK-022-AOT-NO-CE-HOST", fixture.PackedAotConsumerRunOutput, StringComparison.Ordinal);
}
+ [Fact]
+ public void Packaged_AOT_consumer_evaluates_its_publication_contract_and_does_not_suppress_trim_or_AOT_warnings()
+ {
+ Assert.Equal("true", fixture.PackedAotConsumerAllowUnsafeBlocks, true);
+ Assert.Equal("true", fixture.PackedAotConsumerProperties["PublishAot"], true);
+ Assert.Equal("true", fixture.PackedAotConsumerProperties["PublishTrimmed"], true);
+ Assert.Equal("true", fixture.PackedAotConsumerProperties["SelfContained"], true);
+ Assert.Equal("win-x64", fixture.PackedAotConsumerProperties["RuntimeIdentifier"], true);
+ Assert.Equal("true", fixture.PackedAotConsumerProperties["VerifyReferenceAotCompatibility"], true);
+
+ foreach (string warningId in RequiredAotWarningIds)
+ {
+ Assert.Contains(warningId, fixture.PackedAotConsumerWarningsAsErrors, StringComparison.Ordinal);
+ Assert.DoesNotContain(warningId, fixture.PackedAotConsumerProperties["NoWarn"],
+ StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
[Fact]
public void Legacy_Aob_consumer_still_compiles_against_the_packed_package()
{