From 2c9965ab017312855251cea3c00f5532394a7c0d Mon Sep 17 00:00:00 2001 From: Prekzursil Date: Sun, 1 Mar 2026 02:44:04 +0200 Subject: [PATCH 01/30] feat: delta closure for truthful routing and portability Add SWFOC_FORCE_PROMOTED_EXTENDER override semantics in BackendRouter with default non-forced promoted routing, de-trap universal_auto backend preference, remove hardcoded user save-root metadata, and align tests/docs/TODO evidence for deterministic and tactical live validation runs. Co-authored-by: Codex --- TODO.md | 7 + docs/LIVE_VALIDATION_RUNBOOK.md | 20 +- docs/TEST_PLAN.md | 17 +- profiles/default/profiles/base_sweaw.json | 1 - profiles/default/profiles/base_swfoc.json | 1 - profiles/default/profiles/universal_auto.json | 10 +- .../Services/BackendRouter.cs | 47 ++++- .../Profiles/ProfileInheritanceTests.cs | 18 ++ .../ProfileMetadataPortabilityTests.cs | 30 +++ .../Runtime/BackendRouterTests.cs | 182 ++++++++++++++++-- 10 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 tests/SwfocTrainer.Tests/Profiles/ProfileMetadataPortabilityTests.cs diff --git a/TODO.md b/TODO.md index c84c9c0..697a760 100644 --- a/TODO.md +++ b/TODO.md @@ -118,6 +118,13 @@ Reliability rule for runtime/mod tasks: evidence: bundle `TestResults/runs/LIVE-TACTICAL-20260228-211256/repro-bundle.json` evidence: bundle `TestResults/runs/LIVE-AOTR-20260228-211521/repro-bundle.json` evidence: bundle `TestResults/runs/LIVE-ROE-20260228-211757/repro-bundle.json` +- [x] Delta closure wave: default non-forced promoted FoC routing + env override (`SWFOC_FORCE_PROMOTED_EXTENDER`), `universal_auto` backend de-trap (`auto`), and profile metadata save-root portability cleanup. + evidence: test `tests/SwfocTrainer.Tests/Runtime/BackendRouterTests.cs` + evidence: test `tests/SwfocTrainer.Tests/Profiles/ProfileInheritanceTests.cs` + evidence: test `tests/SwfocTrainer.Tests/Profiles/ProfileMetadataPortabilityTests.cs` + evidence: manual `2026-03-01` `dotnet restore SwfocTrainer.sln` + `dotnet build SwfocTrainer.sln -c Release --no-restore` + `dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName!~SwfocTrainer.Tests.Profiles.Live&FullyQualifiedName!~RuntimeAttachSmokeTests"` => `Passed: 233` + evidence: bundle `TestResults/runs/20260301-004145/repro-bundle.json` (`classification=blocked_environment`, tactical default routing run; no swfoc process detected) + evidence: bundle `TestResults/runs/20260301-004232/repro-bundle.json` (`classification=blocked_environment`, tactical forced-override run; no swfoc process detected) ## Later (M2 + M3 + M4) diff --git a/docs/LIVE_VALIDATION_RUNBOOK.md b/docs/LIVE_VALIDATION_RUNBOOK.md index bd250c7..6055ef9 100644 --- a/docs/LIVE_VALIDATION_RUNBOOK.md +++ b/docs/LIVE_VALIDATION_RUNBOOK.md @@ -9,9 +9,11 @@ Use this runbook to gather real-machine evidence for runtime/mod issues and mile - `native/runtime/SwfocExtender.Host.exe` - `SWFOC_EXTENDER_HOST_PATH` for every `dotnet test` subprocess. - If running a live test command manually (outside run-live script), set `SWFOC_EXTENDER_HOST_PATH` explicitly to avoid nondeterministic host resolution. -- Promoted actions are extender-authoritative and fail-closed: - - no managed/memory fallback is accepted for promoted matrix evidence. - - missing or unverified promoted capability must surface fail-closed diagnostics. +- Save-root defaults are runtime-resolved from current user environment (`SaveOptions.DefaultSaveRootPath`); no profile should hardcode per-user save paths. +- Promoted FoC actions default to non-forced routing: + - `SWFOC_FORCE_PROMOTED_EXTENDER` unset/false keeps normal execution-kind routing. + - set `SWFOC_FORCE_PROMOTED_EXTENDER=1` when running extender-authoritative promoted matrix evidence. + - missing or unverified promoted capability under forced mode must surface fail-closed diagnostics. - For AOTR: ensure launch context resolves to `aotr_1397421866_swfoc`. - For ROE: ensure launch context resolves to `roe_3447786229_swfoc`. - From repo root, run on Windows PowerShell. @@ -139,6 +141,12 @@ vNext bundle sections (required for runtime-affecting changes): ## 4. Promoted Action Matrix Evidence (Issue #7) +Set extender-forced routing before promoted matrix closure runs: + +```powershell +$env:SWFOC_FORCE_PROMOTED_EXTENDER = "1" +``` + Promoted matrix evidence must cover 3 profiles x 5 actions (15 total checks): | Action ID | `base_swfoc` | `aotr_1397421866_swfoc` | `roe_3447786229_swfoc` | @@ -168,6 +176,12 @@ Expected evidence behavior for promoted actions: - `hasFallbackMarker=false` - fail-closed outcomes use explicit route diagnostics (`SAFETY_FAIL_CLOSED`) and block issue `#7` closure. +Reset the override after matrix runs: + +```powershell +Remove-Item Env:SWFOC_FORCE_PROMOTED_EXTENDER -ErrorAction SilentlyContinue +``` + `set_unit_cap` promoted matrix contract: - run as a deterministic two-step sequence per profile: diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md index d389f98..bd61338 100644 --- a/docs/TEST_PLAN.md +++ b/docs/TEST_PLAN.md @@ -69,7 +69,8 @@ - verifies feature-flag gate, missing runtime context gate, and mode mismatch blocking - `BackendRouterTests` - verifies fail-closed mutating behavior for hard-extender profiles - - verifies extender route promotion only when capability proof is present + - verifies promoted extender routing is opt-in via `SWFOC_FORCE_PROMOTED_EXTENDER` + - verifies extender route promotion only when capability proof is present under override mode - verifies capability contract blocking and legacy memory fallback behavior - verifies fallback patch actions stay off promoted extender matrix and preserve managed-memory routing - `MainViewModelSessionGatingTests` @@ -193,7 +194,13 @@ For each profile (`base_sweaw`, `base_swfoc`, `aotr_1397421866_swfoc`, `roe_3447 ## Phase 2 promoted action and hotkey checks -Promoted actions are extender-authoritative and fail-closed for FoC profiles. +Promoted FoC actions are not forced to extender by default. +For extender-authoritative promoted matrix evidence, explicitly enable: + +```powershell +$env:SWFOC_FORCE_PROMOTED_EXTENDER = "1" +``` + Evidence must come from `actionStatusDiagnostics` in `repro-bundle.json` (source `live-promoted-action-matrix.json`). Required profile/action matrix: @@ -226,6 +233,12 @@ Verification checklist: - when `top-mods.json` is present, `actionStatusDiagnostics.entries` must include explicit rows for discovered workshop ids (up to 10 mods x 5 promoted actions) - top-mod rows may be `Skipped` during deterministic/live runs that only execute shipped profile matrix, but each skipped row must include `skipReasonCode` +After matrix runs: + +```powershell +Remove-Item Env:SWFOC_FORCE_PROMOTED_EXTENDER -ErrorAction SilentlyContinue +``` + ## Live Ops checklist (M1) For tactical sessions: diff --git a/profiles/default/profiles/base_sweaw.json b/profiles/default/profiles/base_sweaw.json index c4005c1..e93b630 100644 --- a/profiles/default/profiles/base_sweaw.json +++ b/profiles/default/profiles/base_sweaw.json @@ -373,7 +373,6 @@ "metadata": { "profileLineage": "base", "targetExecutable": "sweaw.exe", - "saveRootDefault": "C:\\Users\\Prekzursil\\Saved Games\\Petroglyph", "profileAliases": "base_sweaw,sweaw,empire at war,eaw", "localPathHints": "sweaw,empire at war", "criticalSymbols": "credits,planet_owner,hero_respawn_timer,game_speed,unit_cap", diff --git a/profiles/default/profiles/base_swfoc.json b/profiles/default/profiles/base_swfoc.json index 00fa2f9..fa1a1fd 100644 --- a/profiles/default/profiles/base_swfoc.json +++ b/profiles/default/profiles/base_swfoc.json @@ -426,7 +426,6 @@ "metadata": { "profileLineage": "base", "targetExecutable": "swfoc.exe", - "saveRootDefault": "C:\\Users\\Prekzursil\\Saved Games\\Petroglyph", "profileAliases": "base_swfoc,swfoc,forces of corruption,foc", "localPathHints": "swfoc,corruption,forces of corruption", "criticalSymbols": "credits,planet_owner,hero_respawn_timer,game_speed,unit_cap", diff --git a/profiles/default/profiles/universal_auto.json b/profiles/default/profiles/universal_auto.json index 6310794..00eba33 100644 --- a/profiles/default/profiles/universal_auto.json +++ b/profiles/default/profiles/universal_auto.json @@ -4,14 +4,8 @@ "inherits": "base_swfoc", "exeTarget": "Swfoc", "steamWorkshopId": null, - "backendPreference": "extender", - "requiredCapabilities": [ - "set_credits", - "freeze_timer", - "toggle_fog_reveal", - "set_unit_cap", - "toggle_instant_build_patch" - ], + "backendPreference": "auto", + "requiredCapabilities": [], "hostPreference": "starwarsg_preferred", "experimentalFeatures": [ "overlay_ui" diff --git a/src/SwfocTrainer.Runtime/Services/BackendRouter.cs b/src/SwfocTrainer.Runtime/Services/BackendRouter.cs index 082e659..6f49e5e 100644 --- a/src/SwfocTrainer.Runtime/Services/BackendRouter.cs +++ b/src/SwfocTrainer.Runtime/Services/BackendRouter.cs @@ -5,6 +5,8 @@ namespace SwfocTrainer.Runtime.Services; public sealed class BackendRouter : IBackendRouter { + private const string PromotedExtenderOverrideEnvironmentVariable = "SWFOC_FORCE_PROMOTED_EXTENDER"; + private static readonly HashSet PromotedExtenderActionIds = new(StringComparer.OrdinalIgnoreCase) { "freeze_timer", @@ -59,7 +61,9 @@ private static RouteResolutionState CreateRouteResolutionState( ProcessMetadata process, CapabilityReport capabilityReport) { - var isPromotedExtenderAction = IsPromotedExtenderAction(request.Action.Id, profile, process); + var promotedExtenderOverride = ResolvePromotedExtenderOverrideState(); + var isPromotedActionCandidate = IsPromotedExtenderActionCandidate(request.Action.Id, profile, process); + var isPromotedExtenderAction = promotedExtenderOverride.Enabled && isPromotedActionCandidate; var defaultBackend = MapDefaultBackend(request.Action.ExecutionKind, isPromotedExtenderAction); var preferredBackend = ResolvePreferredBackend(profile.BackendPreference, defaultBackend, isPromotedExtenderAction); var isMutating = IsMutating(request.Action.Id); @@ -81,7 +85,9 @@ private static RouteResolutionState CreateRouteResolutionState( profileRequiredCapabilities, requiredCapabilities, missingRequired, - isPromotedExtenderAction)); + isPromotedExtenderAction, + promotedExtenderOverride.Enabled, + promotedExtenderOverride.Source)); return new RouteResolutionState( PreferredBackend: preferredBackend, MutatingAction: isMutating, @@ -108,6 +114,9 @@ private static RouteResolutionState CreateRouteResolutionState( ["missingRequiredCapabilities"] = context.MissingRequired, ["hybridExecution"] = false, ["promotedExtenderAction"] = context.PromotedExtenderAction, + ["promotedExtenderOverrideEnabled"] = context.PromotedExtenderOverrideEnabled, + ["promotedExtenderOverrideSource"] = context.PromotedExtenderOverrideSource, + ["promotedExtenderApplied"] = context.PromotedExtenderAction, ["capabilityMapReasonCode"] = capabilityMapReasonCode ?? string.Empty, ["capabilityMapState"] = capabilityMapState ?? string.Empty, ["capabilityDeclaredAvailable"] = capabilityDeclaredAvailable @@ -393,7 +402,7 @@ private static string[] ResolveRequiredCapabilitiesForAction( return required.ToArray(); } - private static bool IsPromotedExtenderAction( + private static bool IsPromotedExtenderActionCandidate( string actionId, TrainerProfile profile, ProcessMetadata process) @@ -403,6 +412,30 @@ private static bool IsPromotedExtenderAction( PromotedExtenderActionIds.Contains(actionId); } + private static PromotedExtenderOverrideState ResolvePromotedExtenderOverrideState() + { + var raw = Environment.GetEnvironmentVariable(PromotedExtenderOverrideEnvironmentVariable); + if (string.IsNullOrWhiteSpace(raw)) + { + return new PromotedExtenderOverrideState(Enabled: false, Source: "default"); + } + + if (bool.TryParse(raw, out var parsedBool)) + { + return new PromotedExtenderOverrideState(Enabled: parsedBool, Source: "env"); + } + + if (int.TryParse(raw, out var parsedInt)) + { + return new PromotedExtenderOverrideState(Enabled: parsedInt != 0, Source: "env"); + } + + var normalized = raw.Trim(); + var enabled = normalized.Equals("on", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("yes", StringComparison.OrdinalIgnoreCase); + return new PromotedExtenderOverrideState(Enabled: enabled, Source: "env"); + } + private static bool IsFoCContext(TrainerProfile profile, ProcessMetadata process) { return profile.ExeTarget == ExeTarget.Swfoc || @@ -419,7 +452,9 @@ private readonly record struct BackendDiagnosticsContext( IReadOnlyList ProfileRequiredCapabilities, IReadOnlyList RequiredCapabilities, IReadOnlyList MissingRequired, - bool PromotedExtenderAction); + bool PromotedExtenderAction, + bool PromotedExtenderOverrideEnabled, + string PromotedExtenderOverrideSource); private readonly record struct RouteResolutionState( ExecutionBackendKind PreferredBackend, @@ -427,4 +462,8 @@ private readonly record struct RouteResolutionState( IReadOnlyList MissingRequired, Dictionary Diagnostics, bool PromotedExtenderAction); + + private readonly record struct PromotedExtenderOverrideState( + bool Enabled, + string Source); } diff --git a/tests/SwfocTrainer.Tests/Profiles/ProfileInheritanceTests.cs b/tests/SwfocTrainer.Tests/Profiles/ProfileInheritanceTests.cs index a6dcbe5..96da987 100644 --- a/tests/SwfocTrainer.Tests/Profiles/ProfileInheritanceTests.cs +++ b/tests/SwfocTrainer.Tests/Profiles/ProfileInheritanceTests.cs @@ -50,4 +50,22 @@ public async Task Manifest_Should_List_All_Target_Profiles() "universal_auto" ]); } + + [Fact] + public async Task UniversalAutoProfile_Should_Default_To_AutoBackendPreference() + { + var root = TestPaths.FindRepoRoot(); + var options = new ProfileRepositoryOptions + { + ProfilesRootPath = Path.Combine(root, "profiles", "default") + }; + + var repository = new FileSystemProfileRepository(options); + var profile = await repository.ResolveInheritedProfileAsync("universal_auto"); + + profile.BackendPreference.Should().Be("auto"); + profile.Actions.Should().ContainKey("set_credits"); + profile.Actions.Should().ContainKey("freeze_timer"); + profile.Actions.Should().ContainKey("set_unit_cap"); + } } diff --git a/tests/SwfocTrainer.Tests/Profiles/ProfileMetadataPortabilityTests.cs b/tests/SwfocTrainer.Tests/Profiles/ProfileMetadataPortabilityTests.cs new file mode 100644 index 0000000..e17d611 --- /dev/null +++ b/tests/SwfocTrainer.Tests/Profiles/ProfileMetadataPortabilityTests.cs @@ -0,0 +1,30 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using SwfocTrainer.Tests.Common; +using Xunit; + +namespace SwfocTrainer.Tests.Profiles; + +public sealed class ProfileMetadataPortabilityTests +{ + private static readonly Regex UserScopedWindowsSaveRootPattern = new( + "\"saveRootDefault\"\\s*:\\s*\"[A-Za-z]:\\\\\\\\Users\\\\\\\\", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + [Fact] + public void DefaultProfiles_ShouldNotContain_UserSpecific_SaveRootDefaults() + { + var root = TestPaths.FindRepoRoot(); + var profileDirectory = Path.Combine(root, "profiles", "default", "profiles"); + var profileFiles = Directory.GetFiles(profileDirectory, "*.json", SearchOption.TopDirectoryOnly); + + profileFiles.Should().NotBeEmpty(); + + foreach (var profileFile in profileFiles) + { + var content = File.ReadAllText(profileFile); + UserScopedWindowsSaveRootPattern.IsMatch(content).Should().BeFalse( + because: $"profile metadata in '{Path.GetFileName(profileFile)}' should stay portable across user accounts"); + } + } +} diff --git a/tests/SwfocTrainer.Tests/Runtime/BackendRouterTests.cs b/tests/SwfocTrainer.Tests/Runtime/BackendRouterTests.cs index c2fbd17..559d2ea 100644 --- a/tests/SwfocTrainer.Tests/Runtime/BackendRouterTests.cs +++ b/tests/SwfocTrainer.Tests/Runtime/BackendRouterTests.cs @@ -14,10 +14,11 @@ public sealed class BackendRouterTests [InlineData("toggle_ai", ExecutionKind.Memory)] [InlineData("set_unit_cap", ExecutionKind.CodePatch)] [InlineData("toggle_instant_build_patch", ExecutionKind.CodePatch)] - public void Resolve_ShouldPromoteHybridActions_ToExtender_WhenCapabilityIsAvailable( + public void Resolve_ShouldKeepLegacyBackend_ForPromotedActions_WhenOverrideDisabled( string actionId, ExecutionKind executionKind) { + using var _ = PromotedExtenderOverrideScope.Disable(); var router = new BackendRouter(); var request = BuildRequest( actionId, @@ -46,12 +47,18 @@ public void Resolve_ShouldPromoteHybridActions_ToExtender_WhenCapabilityIsAvaila var decision = router.Resolve(request, profile, process, report); decision.Allowed.Should().BeTrue(); - decision.Backend.Should().Be(ExecutionBackendKind.Extender); + decision.Backend.Should().Be(ExecutionBackendKind.Memory); decision.ReasonCode.Should().Be(RuntimeReasonCode.CAPABILITY_PROBE_PASS); decision.Diagnostics.Should().ContainKey("hybridExecution"); decision.Diagnostics!["hybridExecution"].Should().Be(false); decision.Diagnostics.Should().ContainKey("promotedExtenderAction"); - decision.Diagnostics!["promotedExtenderAction"].Should().Be(true); + decision.Diagnostics!["promotedExtenderAction"].Should().Be(false); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideEnabled"); + decision.Diagnostics!["promotedExtenderOverrideEnabled"].Should().Be(false); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideSource"); + decision.Diagnostics!["promotedExtenderOverrideSource"].Should().Be("default"); + decision.Diagnostics.Should().ContainKey("promotedExtenderApplied"); + decision.Diagnostics!["promotedExtenderApplied"].Should().Be(false); decision.Diagnostics.Should().ContainKey("capabilityMapReasonCode"); decision.Diagnostics!["capabilityMapReasonCode"].Should().Be("CAPABILITY_PROBE_PASS"); decision.Diagnostics.Should().ContainKey("capabilityMapState"); @@ -66,10 +73,11 @@ public void Resolve_ShouldPromoteHybridActions_ToExtender_WhenCapabilityIsAvaila [InlineData("toggle_ai", ExecutionKind.Memory)] [InlineData("set_unit_cap", ExecutionKind.CodePatch)] [InlineData("toggle_instant_build_patch", ExecutionKind.CodePatch)] - public void Resolve_ShouldBlockHybridActions_WhenCapabilityIsMissing( + public void Resolve_ShouldKeepLegacyBackend_ForPromotedActions_WhenCapabilityIsMissing_AndOverrideDisabled( string actionId, ExecutionKind executionKind) { + using var _ = PromotedExtenderOverrideScope.Disable(); var router = new BackendRouter(); var request = BuildRequest(actionId, executionKind); var profile = BuildProfile(backendPreference: "auto"); @@ -78,13 +86,19 @@ public void Resolve_ShouldBlockHybridActions_WhenCapabilityIsMissing( var decision = router.Resolve(request, profile, process, report); - decision.Allowed.Should().BeFalse(); - decision.Backend.Should().Be(ExecutionBackendKind.Extender); - decision.ReasonCode.Should().Be(RuntimeReasonCode.CAPABILITY_REQUIRED_MISSING); + decision.Allowed.Should().BeTrue(); + decision.Backend.Should().Be(ExecutionBackendKind.Memory); + decision.ReasonCode.Should().Be(RuntimeReasonCode.CAPABILITY_PROBE_PASS); decision.Diagnostics.Should().ContainKey("hybridExecution"); decision.Diagnostics!["hybridExecution"].Should().Be(false); decision.Diagnostics.Should().ContainKey("promotedExtenderAction"); - decision.Diagnostics!["promotedExtenderAction"].Should().Be(true); + decision.Diagnostics!["promotedExtenderAction"].Should().Be(false); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideEnabled"); + decision.Diagnostics!["promotedExtenderOverrideEnabled"].Should().Be(false); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideSource"); + decision.Diagnostics!["promotedExtenderOverrideSource"].Should().Be("default"); + decision.Diagnostics.Should().ContainKey("promotedExtenderApplied"); + decision.Diagnostics!["promotedExtenderApplied"].Should().Be(false); decision.Diagnostics.Should().NotContainKey("fallbackBackend"); } @@ -94,10 +108,11 @@ public void Resolve_ShouldBlockHybridActions_WhenCapabilityIsMissing( [InlineData("toggle_ai", ExecutionKind.Memory)] [InlineData("set_unit_cap", ExecutionKind.CodePatch)] [InlineData("toggle_instant_build_patch", ExecutionKind.CodePatch)] - public void Resolve_ShouldFailClosedForPromotedActions_WhenCapabilityIsUnverified( + public void Resolve_ShouldKeepLegacyBackend_ForPromotedActions_WhenCapabilityIsUnverified_AndOverrideDisabled( string actionId, ExecutionKind executionKind) { + using var _ = PromotedExtenderOverrideScope.Disable(); var router = new BackendRouter(); var request = BuildRequest(actionId, executionKind); var profile = BuildProfile(backendPreference: "auto"); @@ -117,16 +132,130 @@ public void Resolve_ShouldFailClosedForPromotedActions_WhenCapabilityIsUnverifie var decision = router.Resolve(request, profile, process, report); - decision.Allowed.Should().BeFalse(); - decision.Backend.Should().Be(ExecutionBackendKind.Extender); - decision.ReasonCode.Should().Be(RuntimeReasonCode.SAFETY_FAIL_CLOSED); + decision.Allowed.Should().BeTrue(); + decision.Backend.Should().Be(ExecutionBackendKind.Memory); + decision.ReasonCode.Should().Be(RuntimeReasonCode.CAPABILITY_PROBE_PASS); decision.Diagnostics.Should().ContainKey("hybridExecution"); decision.Diagnostics!["hybridExecution"].Should().Be(false); decision.Diagnostics.Should().ContainKey("promotedExtenderAction"); - decision.Diagnostics!["promotedExtenderAction"].Should().Be(true); + decision.Diagnostics!["promotedExtenderAction"].Should().Be(false); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideEnabled"); + decision.Diagnostics!["promotedExtenderOverrideEnabled"].Should().Be(false); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideSource"); + decision.Diagnostics!["promotedExtenderOverrideSource"].Should().Be("default"); + decision.Diagnostics.Should().ContainKey("promotedExtenderApplied"); + decision.Diagnostics!["promotedExtenderApplied"].Should().Be(false); decision.Diagnostics.Should().NotContainKey("fallbackBackend"); } + [Theory] + [InlineData("freeze_timer", ExecutionKind.Memory)] + [InlineData("toggle_fog_reveal", ExecutionKind.Memory)] + [InlineData("toggle_ai", ExecutionKind.Memory)] + [InlineData("set_unit_cap", ExecutionKind.CodePatch)] + [InlineData("toggle_instant_build_patch", ExecutionKind.CodePatch)] + public void Resolve_ShouldPromoteActions_ToExtender_WhenOverrideEnabled_AndCapabilityIsVerified( + string actionId, + ExecutionKind executionKind) + { + using var _ = PromotedExtenderOverrideScope.Enable(); + var router = new BackendRouter(); + var request = BuildRequest(actionId, executionKind); + var profile = BuildProfile(backendPreference: "auto"); + var process = BuildProcess(); + var report = new CapabilityReport( + profile.Id, + DateTimeOffset.UtcNow, + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [actionId] = new BackendCapability( + actionId, + Available: true, + CapabilityConfidenceState.Verified, + RuntimeReasonCode.CAPABILITY_PROBE_PASS) + }, + RuntimeReasonCode.CAPABILITY_PROBE_PASS); + + var decision = router.Resolve(request, profile, process, report); + + decision.Allowed.Should().BeTrue(); + decision.Backend.Should().Be(ExecutionBackendKind.Extender); + decision.ReasonCode.Should().Be(RuntimeReasonCode.CAPABILITY_PROBE_PASS); + decision.Diagnostics.Should().ContainKey("promotedExtenderAction"); + decision.Diagnostics!["promotedExtenderAction"].Should().Be(true); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideEnabled"); + decision.Diagnostics!["promotedExtenderOverrideEnabled"].Should().Be(true); + decision.Diagnostics.Should().ContainKey("promotedExtenderOverrideSource"); + decision.Diagnostics!["promotedExtenderOverrideSource"].Should().Be("env"); + decision.Diagnostics.Should().ContainKey("promotedExtenderApplied"); + decision.Diagnostics!["promotedExtenderApplied"].Should().Be(true); + } + + [Theory] + [InlineData("freeze_timer", ExecutionKind.Memory)] + [InlineData("toggle_fog_reveal", ExecutionKind.Memory)] + [InlineData("toggle_ai", ExecutionKind.Memory)] + [InlineData("set_unit_cap", ExecutionKind.CodePatch)] + [InlineData("toggle_instant_build_patch", ExecutionKind.CodePatch)] + public void Resolve_ShouldBlockPromotedActions_WhenOverrideEnabled_AndCapabilityIsMissing( + string actionId, + ExecutionKind executionKind) + { + using var _ = PromotedExtenderOverrideScope.Enable(); + var router = new BackendRouter(); + var request = BuildRequest(actionId, executionKind); + var profile = BuildProfile(backendPreference: "auto"); + var process = BuildProcess(); + var report = CapabilityReport.Unknown(profile.Id, RuntimeReasonCode.CAPABILITY_UNKNOWN); + + var decision = router.Resolve(request, profile, process, report); + + decision.Allowed.Should().BeFalse(); + decision.Backend.Should().Be(ExecutionBackendKind.Extender); + decision.ReasonCode.Should().Be(RuntimeReasonCode.CAPABILITY_REQUIRED_MISSING); + decision.Diagnostics.Should().ContainKey("promotedExtenderAction"); + decision.Diagnostics!["promotedExtenderAction"].Should().Be(true); + decision.Diagnostics.Should().ContainKey("promotedExtenderApplied"); + decision.Diagnostics!["promotedExtenderApplied"].Should().Be(true); + } + + [Theory] + [InlineData("freeze_timer", ExecutionKind.Memory)] + [InlineData("toggle_fog_reveal", ExecutionKind.Memory)] + [InlineData("toggle_ai", ExecutionKind.Memory)] + [InlineData("set_unit_cap", ExecutionKind.CodePatch)] + [InlineData("toggle_instant_build_patch", ExecutionKind.CodePatch)] + public void Resolve_ShouldFailClosedForPromotedActions_WhenOverrideEnabled_AndCapabilityIsUnverified( + string actionId, + ExecutionKind executionKind) + { + using var _ = PromotedExtenderOverrideScope.Enable(); + var router = new BackendRouter(); + var request = BuildRequest(actionId, executionKind); + var profile = BuildProfile(backendPreference: "auto"); + var process = BuildProcess(); + var report = new CapabilityReport( + profile.Id, + DateTimeOffset.UtcNow, + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [actionId] = new BackendCapability( + actionId, + Available: true, + Confidence: CapabilityConfidenceState.Experimental, + ReasonCode: RuntimeReasonCode.CAPABILITY_FEATURE_EXPERIMENTAL) + }, + RuntimeReasonCode.CAPABILITY_PROBE_PASS); + + var decision = router.Resolve(request, profile, process, report); + + decision.Allowed.Should().BeFalse(); + decision.Backend.Should().Be(ExecutionBackendKind.Extender); + decision.ReasonCode.Should().Be(RuntimeReasonCode.SAFETY_FAIL_CLOSED); + decision.Diagnostics.Should().ContainKey("promotedExtenderApplied"); + decision.Diagnostics!["promotedExtenderApplied"].Should().Be(true); + } + [Fact] public void Resolve_ShouldFailClosed_WhenExtenderIsRequiredButCapabilityUnknownForMutation() { @@ -367,4 +496,31 @@ private static ProcessMetadata BuildProcess( WorkshopMatchCount: 1, SelectionScore: 1311.0d); } + + private sealed class PromotedExtenderOverrideScope : IDisposable + { + private const string VariableName = "SWFOC_FORCE_PROMOTED_EXTENDER"; + private readonly string? _previousValue; + + private PromotedExtenderOverrideScope(string? value) + { + _previousValue = Environment.GetEnvironmentVariable(VariableName); + Environment.SetEnvironmentVariable(VariableName, value); + } + + public static PromotedExtenderOverrideScope Disable() + { + return new PromotedExtenderOverrideScope(value: null); + } + + public static PromotedExtenderOverrideScope Enable() + { + return new PromotedExtenderOverrideScope(value: "1"); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(VariableName, _previousValue); + } + } } From d7cde91dfe1826331908d082ac82654b5f04ec33 Mon Sep 17 00:00:00 2001 From: Prekzursil Date: Sun, 1 Mar 2026 19:27:09 +0200 Subject: [PATCH 02/30] M3 closure: helper bridge ingress, launch+attach, strict tactical modes Implement helper bridge contracts/backend wiring, native helper plugin routing, strict tactical mode split, and launch automation in runtime/UI. Stabilize deterministic/live validation by isolating named-pipe tests, hardening repro bundle parsing, improving ROE auto-launch mod args, and recording latest evidence run IDs in docs/TODO. Co-authored-by: Codex --- TODO.md | 12 + docs/LIVE_VALIDATION_RUNBOOK.md | 27 ++- docs/TEST_PLAN.md | 18 +- .../src/BridgeHostMain.cpp | 65 +++++- native/SwfocExtender.Plugins/CMakeLists.txt | 3 +- .../plugins/HelperLuaPlugin.hpp | 18 ++ .../plugins/PluginContracts.hpp | 7 + .../src/BuildPatchPlugin.cpp | 2 +- .../src/HelperLuaPlugin.cpp | 154 +++++++++++++ .../profiles/aotr_1397421866_swfoc.json | 8 + profiles/default/profiles/base_sweaw.json | 24 +- profiles/default/profiles/base_swfoc.json | 28 ++- .../profiles/roe_3447786229_swfoc.json | 6 + src/SwfocTrainer.App/MainWindow.xaml | 7 + src/SwfocTrainer.App/Program.cs | 4 + .../ViewModels/MainViewModel.cs | 39 +++- .../MainViewModelBindableMembersBase.cs | 31 +++ .../ViewModels/MainViewModelCoreStateBase.cs | 10 +- .../ViewModels/MainViewModelDefaults.cs | 2 + .../ViewModels/MainViewModelDependencies.cs | 2 + .../ViewModels/MainViewModelFactories.cs | 3 + ...MainViewModelRuntimeModeOverrideHelpers.cs | 26 ++- .../Contracts/IGameLaunchService.cs | 13 ++ .../Contracts/IHelperBridgeBackend.cs | 20 ++ src/SwfocTrainer.Core/Models/Enums.cs | 4 +- .../Models/GameLaunchModels.cs | 30 +++ .../Models/HelperBridgeModels.cs | 24 ++ src/SwfocTrainer.Core/Models/ProfileModels.cs | 2 + .../Models/RuntimeReasonCode.cs | 7 + .../Models/SdkOperationCatalog.cs | 26 ++- .../Services/ActionReliabilityService.cs | 23 ++ .../SelectedUnitTransactionService.cs | 2 +- .../Services/TrainerOrchestrator.cs | 40 +++- .../Services/GameLaunchService.cs | 149 +++++++++++++ .../Services/NamedPipeExtenderBackend.cs | 41 +++- .../Services/NamedPipeHelperBridgeBackend.cs | 208 ++++++++++++++++++ .../Services/ProcessLocator.cs | 12 +- .../Services/RuntimeAdapter.State.cs | 1 + .../Services/RuntimeAdapter.cs | 146 ++++++++++-- .../Services/RuntimeModeProbeResolver.cs | 8 +- .../Services/TelemetryLogTailService.cs | 20 +- .../MainViewModelRuntimeModeOverrideTests.cs | 6 +- .../Core/ActionReliabilityServiceTests.cs | 6 +- .../SelectedUnitTransactionServiceTests.cs | 16 +- .../Core/TrainerOrchestratorTests.cs | 2 +- .../Profiles/LiveHeroHelperWorkflowTests.cs | 11 +- .../Profiles/LivePromotedActionMatrixTests.cs | 29 +++ .../Profiles/LiveRoeRuntimeHealthTests.cs | 7 + .../LiveTacticalToggleWorkflowTests.cs | 6 +- .../Profiles/ModCalibrationServiceTests.cs | 2 +- .../ProfileMetadataPortabilityTests.cs | 25 +++ .../Runtime/NamedPipeExtenderBackendTests.cs | 33 ++- .../NamedPipeHelperBridgeBackendTests.cs | 185 ++++++++++++++++ .../RuntimeAdapterModeOverrideTests.cs | 14 +- .../RuntimeAdapterPromotedAliasTests.cs | 2 +- .../Runtime/RuntimeModeProbeResolverTests.cs | 6 +- .../Runtime/SymbolHealthServiceTests.cs | 2 +- .../Runtime/TelemetryLogTailServiceTests.cs | 4 +- tools/collect-mod-repro-bundle.ps1 | 73 +++++- tools/run-live-validation.ps1 | 186 ++++++++++++++++ tools/schemas/repro-bundle.schema.json | 4 + 61 files changed, 1738 insertions(+), 153 deletions(-) create mode 100644 native/SwfocExtender.Plugins/include/swfoc_extender/plugins/HelperLuaPlugin.hpp create mode 100644 native/SwfocExtender.Plugins/src/HelperLuaPlugin.cpp create mode 100644 src/SwfocTrainer.Core/Contracts/IGameLaunchService.cs create mode 100644 src/SwfocTrainer.Core/Contracts/IHelperBridgeBackend.cs create mode 100644 src/SwfocTrainer.Core/Models/GameLaunchModels.cs create mode 100644 src/SwfocTrainer.Core/Models/HelperBridgeModels.cs create mode 100644 src/SwfocTrainer.Runtime/Services/GameLaunchService.cs create mode 100644 src/SwfocTrainer.Runtime/Services/NamedPipeHelperBridgeBackend.cs create mode 100644 tests/SwfocTrainer.Tests/Runtime/NamedPipeHelperBridgeBackendTests.cs diff --git a/TODO.md b/TODO.md index 697a760..8433c85 100644 --- a/TODO.md +++ b/TODO.md @@ -125,6 +125,18 @@ Reliability rule for runtime/mod tasks: evidence: manual `2026-03-01` `dotnet restore SwfocTrainer.sln` + `dotnet build SwfocTrainer.sln -c Release --no-restore` + `dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName!~SwfocTrainer.Tests.Profiles.Live&FullyQualifiedName!~RuntimeAttachSmokeTests"` => `Passed: 233` evidence: bundle `TestResults/runs/20260301-004145/repro-bundle.json` (`classification=blocked_environment`, tactical default routing run; no swfoc process detected) evidence: bundle `TestResults/runs/20260301-004232/repro-bundle.json` (`classification=blocked_environment`, tactical forced-override run; no swfoc process detected) +- [x] M3 closure wave: helper bridge fail-closed runtime path, `Launch + Attach` automation, strict tactical mode split (`TacticalLand`/`TacticalSpace`/`AnyTactical`), and codex-owned live process matrix rerun with schema-validated bundles. + evidence: test `tests/SwfocTrainer.Tests/Runtime/NamedPipeHelperBridgeBackendTests.cs` + evidence: test `tests/SwfocTrainer.Tests/Runtime/NamedPipeExtenderBackendTests.cs` + evidence: test `tests/SwfocTrainer.Tests/Profiles/LivePromotedActionMatrixTests.cs` + evidence: test `tests/SwfocTrainer.Tests/Profiles/LiveRoeRuntimeHealthTests.cs` + evidence: manual `2026-03-01` `dotnet restore SwfocTrainer.sln` + `dotnet build SwfocTrainer.sln -c Release --no-restore` + `dotnet test tests/SwfocTrainer.Tests/SwfocTrainer.Tests.csproj -c Release --no-build --filter "FullyQualifiedName!~SwfocTrainer.Tests.Profiles.Live&FullyQualifiedName!~RuntimeAttachSmokeTests"` => `Passed: 237` + evidence: manual `2026-03-01` `powershell.exe -File tools/validate-workshop-topmods.ps1 -Path tools/fixtures/workshop_topmods_sample.json -Strict` => `validation passed` + evidence: manual `2026-03-01` `powershell.exe -File tools/validate-generated-profile-seed.ps1 -Path tools/fixtures/generated_profile_seeds_sample.json -Strict` => `validation passed` + evidence: manual `2026-03-01` Session A EAW snapshot `TestResults/runs/LIVE-EAW-20260301-191639/eaw-process-snapshot.json` + evidence: bundle `TestResults/runs/20260301-164213/repro-bundle.json` (`classification=passed`, scope `TACTICAL`) + evidence: bundle `TestResults/runs/20260301-165502/repro-bundle.json` (`classification=passed`, scope `AOTR`) + evidence: bundle `TestResults/runs/20260301-171325/repro-bundle.json` (`classification=skipped`, scope `ROE`, reason `set_credits precondition unmet: hook sync tick not observed`) ## Later (M2 + M3 + M4) diff --git a/docs/LIVE_VALIDATION_RUNBOOK.md b/docs/LIVE_VALIDATION_RUNBOOK.md index 6055ef9..6977336 100644 --- a/docs/LIVE_VALIDATION_RUNBOOK.md +++ b/docs/LIVE_VALIDATION_RUNBOOK.md @@ -4,7 +4,8 @@ Use this runbook to gather real-machine evidence for runtime/mod issues and mile ## 1. Preconditions -- Launch the target game session first (`swfoc.exe` / `StarWarsG.exe`). +- Preferred: let Codex/tooling launch target sessions (`-AutoLaunch`) so run artifacts include deterministic launch wiring. +- Manual prelaunch remains supported when `-AutoLaunch` is not used. - `tools/run-live-validation.ps1` preflights native host build and wires: - `native/runtime/SwfocExtender.Host.exe` - `SWFOC_EXTENDER_HOST_PATH` for every `dotnet test` subprocess. @@ -20,6 +21,11 @@ Use this runbook to gather real-machine evidence for runtime/mod issues and mile - For telemetry-first runtime mode detection, install/drop the telemetry mod template: - `mods/SwfocTrainerTelemetry/Data/Scripts/TelemetryModeEmitter.lua` - expected log marker: `SWFOC_TRAINER_TELEMETRY timestamp= mode=` +- Runtime mode contract is strict: + - `TacticalLand` + - `TacticalSpace` + - `AnyTactical` + - legacy `Tactical` should not be used in profile/runtime contracts. ## 2. Run Pack Command @@ -28,6 +34,7 @@ pwsh ./tools/run-live-validation.ps1 ` -Configuration Release ` -NoBuild ` -Scope FULL ` + -AutoLaunch ` -EmitReproBundle $true ` -FailOnMissingArtifacts ` -Strict @@ -40,6 +47,7 @@ pwsh ./tools/run-live-validation.ps1 ` -Configuration Release ` -NoBuild ` -Scope ROE ` + -AutoLaunch ` -EmitReproBundle $true ` -Strict ` -RequireNonBlockedClassification @@ -51,9 +59,16 @@ Optional scope-specific runs: pwsh ./tools/run-live-validation.ps1 -NoBuild -Scope AOTR -EmitReproBundle $true pwsh ./tools/run-live-validation.ps1 -NoBuild -Scope ROE -EmitReproBundle $true pwsh ./tools/run-live-validation.ps1 -NoBuild -Scope TACTICAL -EmitReproBundle $true +pwsh ./tools/run-live-validation.ps1 -NoBuild -Scope AOTR -AutoLaunch -EmitReproBundle $true +pwsh ./tools/run-live-validation.ps1 -NoBuild -Scope ROE -AutoLaunch -EmitReproBundle $true -ForceWorkshopIds 1397421866,3447786229 -ForceProfileId roe_3447786229_swfoc pwsh ./tools/run-live-validation.ps1 -NoBuild -Scope ROE -EmitReproBundle $true -TopModsPath TestResults/mod-discovery//top-mods.json ``` +Notes for ROE auto-launch: + +- When `-ForceWorkshopIds` includes multiple IDs (for example `1397421866,3447786229`), the launcher emits chained args (`STEAMMOD=1397421866 STEAMMOD=3447786229`) to preserve mod dependency ordering. +- Runtime-health `set_credits` may be skipped with explicit precondition reason when no galactic/campaign sync tick is observed. + Forced-context closure run (for hosts that expose only `StarWarsG.exe NOARTPROCESS IGNOREASSERTS`): ```powershell @@ -112,6 +127,11 @@ vNext bundle sections (required for runtime-affecting changes): - `hookInstallReport` - `overlayState` - `actionStatusDiagnostics` (promoted action matrix diagnostics from `live-promoted-action-matrix.json`) +- helper ingress diagnostics in `repro-bundle.json` diagnostics: + - `helperBridgeState` + - `helperEntryPoint` + - `helperInvocationSource` + - `helperVerifyState` ## 3a. Universal Compatibility Boundary @@ -138,6 +158,10 @@ vNext bundle sections (required for runtime-affecting changes): - `runtimeModeTelemetryReasonCode` - `runtimeModeTelemetrySource=telemetry` - When telemetry is stale or unavailable, expect explicit diagnostics reason codes (for example `telemetry_stale`). +- Mode mapping expectations: + - `LAND` -> `TacticalLand` + - `SPACE` -> `TacticalSpace` + - ambiguous tactical probes should resolve to `AnyTactical`, not legacy `Tactical`. ## 4. Promoted Action Matrix Evidence (Issue #7) @@ -175,6 +199,7 @@ Expected evidence behavior for promoted actions: - `hybridExecution=false` - `hasFallbackMarker=false` - fail-closed outcomes use explicit route diagnostics (`SAFETY_FAIL_CLOSED`) and block issue `#7` closure. +- capability-gated unavailability is recorded as explicit skip (`skipReasonCode=promoted_capability_unavailable`) instead of synthetic pass/fail. Reset the override after matrix runs: diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md index bd61338..0efe8b3 100644 --- a/docs/TEST_PLAN.md +++ b/docs/TEST_PLAN.md @@ -62,7 +62,7 @@ - `LaunchContextResolverTests` - verifies metadata-driven workshop/modpath precedence and generic profile reason-code routing - `RuntimeModeProbeResolverTests` - - verifies runtime-effective tactical/galactic inference from symbol-health probes + - verifies runtime-effective strict mode inference (`TacticalLand` / `TacticalSpace` / `AnyTactical` / `Galactic`) from symbol-health probes - `SdkExecutionGuardTests` - verifies degraded-read allowance and mutating fail-closed behavior - `SdkOperationRouterTests` @@ -87,9 +87,11 @@ - verifies precedence ordering (`MODPATH` > game loose > enabled MEGs) - verifies provenance and shadow metadata (`sourceType`, `sourcePath`, `overrideRank`, `shadowedBy`) - `TelemetryLogTailServiceTests` - - verifies telemetry marker parsing, freshness gating, and stale-ignore behavior + - verifies telemetry marker parsing with strict LAND/SPACE mapping, freshness gating, and stale-ignore behavior - `RuntimeAdapterModeOverrideTests` - - verifies mode precedence with telemetry feed (manual override still highest priority) + - verifies mode precedence with telemetry feed (manual override still highest priority) across strict tactical mode values +- `NamedPipeHelperBridgeBackendTests` + - verifies helper bridge fail-closed behavior and verification-contract enforcement before helper success is reported - `StoryFlowGraphExporterTests` - verifies deterministic node/edge graph output and tactical/galactic event linkage - `LuaHarnessRunnerTests` @@ -167,14 +169,14 @@ pwsh ./tools/validate-ghidra-artifact-index.ps1 -Path tools/fixtures/ghidra_arti For each profile (`base_sweaw`, `base_swfoc`, `aotr_1397421866_swfoc`, `roe_3447786229_swfoc`): -1. Launch game + target mode. +1. Launch target session (prefer app `Launch + Attach` or `tools/run-live-validation.ps1 -AutoLaunch`) + target mode. 2. Load profile and attach. 3. Execute: - credits change - timer freeze toggle - fog reveal toggle - - selected unit HP/shield/speed edit (tactical) + - selected unit HP/shield/speed edit (`AnyTactical` / `TacticalLand` / `TacticalSpace`) - helper spawn action - capture status diagnostics showing `backendRoute`, `routeReasonCode`, `capabilityProbeReasonCode`, `capabilityMapReasonCode`, `capabilityMapState`, `capabilityDeclaredAvailable`, plus `hookState`/`hybridExecution` when present @@ -220,12 +222,12 @@ Verification checklist: - summary: `total`, `passed`, `failed`, `skipped` - each entry: `profileId`, `actionId`, `outcome`, `backendRoute`, `routeReasonCode`, `capabilityProbeReasonCode`, `hybridExecution`, `hasFallbackMarker`, `message`, `skipReasonCode` 4. Verify promoted matrix entry outcomes for issue `#7` closure gate: - - `summary.total=15`, `summary.passed=15`, `summary.failed=0`, `summary.skipped=0` + - `summary.failed=0` - all entries report `backendRoute=Extender` - - all entries report `routeReasonCode=CAPABILITY_PROBE_PASS` - - all entries report `capabilityProbeReasonCode=CAPABILITY_PROBE_PASS` - all entries report `hybridExecution=false` - all entries report `hasFallbackMarker=false` + - normal pass entries report `routeReasonCode=CAPABILITY_PROBE_PASS` and `capabilityProbeReasonCode=CAPABILITY_PROBE_PASS` + - capability-gated entries use explicit skip semantics (`skipReasonCode=promoted_capability_unavailable`) instead of synthetic success. 5. Verify fail-closed behavior remains explicit when environment is unhealthy: - promoted actions must not silently route to fallback backend - blocked runs surface explicit reason codes and must not be used for issue `#7` closure diff --git a/native/SwfocExtender.Bridge/src/BridgeHostMain.cpp b/native/SwfocExtender.Bridge/src/BridgeHostMain.cpp index d2f93ec..8f70870 100644 --- a/native/SwfocExtender.Bridge/src/BridgeHostMain.cpp +++ b/native/SwfocExtender.Bridge/src/BridgeHostMain.cpp @@ -3,6 +3,7 @@ #include "swfoc_extender/plugins/BuildPatchPlugin.hpp" #include "swfoc_extender/plugins/EconomyPlugin.hpp" #include "swfoc_extender/plugins/GlobalTogglePlugin.hpp" +#include "swfoc_extender/plugins/HelperLuaPlugin.hpp" #include "swfoc_extender/plugins/ProcessMutationHelpers.hpp" #include @@ -42,6 +43,7 @@ using swfoc::extender::plugins::CapabilitySnapshot; using swfoc::extender::plugins::CapabilityState; using swfoc::extender::plugins::EconomyPlugin; using swfoc::extender::plugins::GlobalTogglePlugin; +using swfoc::extender::plugins::HelperLuaPlugin; using swfoc::extender::plugins::PluginRequest; using swfoc::extender::plugins::PluginResult; namespace process_mutation = swfoc::extender::plugins::process_mutation; @@ -55,13 +57,16 @@ using swfoc::extender::bridge::host_json::TryReadInt; constexpr const char* kBackendName = "extender"; constexpr const char* kDefaultPipeName = "SwfocExtenderBridge"; -constexpr std::array kSupportedFeatures { +constexpr std::array kSupportedFeatures { "freeze_timer", "toggle_fog_reveal", "toggle_ai", "set_unit_cap", "toggle_instant_build_patch", - "set_credits"}; + "set_credits", + "spawn_unit_helper", + "set_hero_state_helper", + "toggle_roe_respawn_helper"}; /* Cppcheck note (targeted): if cppcheck runs without STL/Windows SDK include paths, @@ -115,6 +120,13 @@ PluginRequest BuildPluginRequest(const BridgeCommand& command) { request.processId = ResolveProcessId(command); request.anchors = ResolveAnchors(command); request.lockValue = ResolveLockCredits(command.payloadJson); + request.helperHookId = ExtractStringValue(command.payloadJson, "helperHookId"); + request.helperEntryPoint = ExtractStringValue(command.payloadJson, "helperEntryPoint"); + request.helperScript = ExtractStringValue(command.payloadJson, "helperScript"); + request.unitId = ExtractStringValue(command.payloadJson, "unitId"); + request.entryMarker = ExtractStringValue(command.payloadJson, "entryMarker"); + request.faction = ExtractStringValue(command.payloadJson, "faction"); + request.globalKey = ExtractStringValue(command.payloadJson, "globalKey"); int intValue = 0; if (TryReadInt(command.payloadJson, "intValue", intValue)) { @@ -252,6 +264,21 @@ void AddProbeFeature( snapshot.features.emplace(featureId, BuildProbeState(probe)); } +void AddHelperProbeFeature( + CapabilitySnapshot& snapshot, + const PluginRequest& probeContext, + const char* featureId) { + CapabilityState state {}; + state.available = probeContext.processId > 0; + state.state = state.available ? "Verified" : "Unavailable"; + state.reasonCode = state.available ? "CAPABILITY_PROBE_PASS" : "HELPER_BRIDGE_UNAVAILABLE"; + state.diagnostics = { + {"probeSource", "native_helper_bridge"}, + {"processId", std::to_string(probeContext.processId)}, + {"helperBridgeState", state.available ? "ready" : "unavailable"}}; + snapshot.features.emplace(featureId, state); +} + CapabilitySnapshot BuildCapabilityProbeSnapshot(const PluginRequest& probeContext) { CapabilitySnapshot snapshot {}; @@ -264,7 +291,10 @@ CapabilitySnapshot BuildCapabilityProbeSnapshot(const PluginRequest& probeContex snapshot, probeContext, "toggle_instant_build_patch", - {"instant_build_patch_injection", "instant_build_patch", "toggle_instant_build_patch"}); + {"instant_build_patch_injection", "instant_build_patch", "instant_build", "toggle_instant_build_patch"}); + AddHelperProbeFeature(snapshot, probeContext, "spawn_unit_helper"); + AddHelperProbeFeature(snapshot, probeContext, "set_hero_state_helper"); + AddHelperProbeFeature(snapshot, probeContext, "toggle_roe_respawn_helper"); EnsureCapabilityEntries(snapshot); return snapshot; @@ -402,6 +432,11 @@ BridgeResult BuildPatchResult(const BridgeCommand& command, BuildPatchPlugin& bu return BuildBridgeResultFromPlugin(command, pluginRequest, buildPatchPlugin.execute(pluginRequest)); } +BridgeResult BuildHelperResult(const BridgeCommand& command, HelperLuaPlugin& helperLuaPlugin) { + auto pluginRequest = BuildPluginRequest(command); + return BuildBridgeResultFromPlugin(command, pluginRequest, helperLuaPlugin.execute(pluginRequest)); +} + BridgeResult BuildUnsupportedFeatureResult(const BridgeCommand& command) { return BuildBridgeResult( command, false, "CAPABILITY_REQUIRED_MISSING", "DENIED", "Feature not supported by current extender host.", "{\"featureId\":\"" + EscapeJson(command.featureId) + "\"}"); @@ -411,7 +446,8 @@ BridgeResult HandleBridgeCommand( const BridgeCommand& command, EconomyPlugin& economyPlugin, GlobalTogglePlugin& globalTogglePlugin, - BuildPatchPlugin& buildPatchPlugin) { + BuildPatchPlugin& buildPatchPlugin, + HelperLuaPlugin& helperLuaPlugin) { if (command.featureId == "health") { return BuildHealthResult(command); } @@ -434,6 +470,12 @@ BridgeResult HandleBridgeCommand( return BuildGlobalToggleResult(command, globalTogglePlugin); } + if (command.featureId == "spawn_unit_helper" || + command.featureId == "set_hero_state_helper" || + command.featureId == "toggle_roe_respawn_helper") { + return BuildHelperResult(command, helperLuaPlugin); + } + return BuildPatchResult(command, buildPatchPlugin); } @@ -477,9 +519,10 @@ void ConfigureBridgeHandler( NamedPipeBridgeServer& server, EconomyPlugin& economyPlugin, GlobalTogglePlugin& globalTogglePlugin, - BuildPatchPlugin& buildPatchPlugin) { - server.setHandler([&economyPlugin, &globalTogglePlugin, &buildPatchPlugin](const BridgeCommand& command) { - return HandleBridgeCommand(command, economyPlugin, globalTogglePlugin, buildPatchPlugin); + BuildPatchPlugin& buildPatchPlugin, + HelperLuaPlugin& helperLuaPlugin) { + server.setHandler([&economyPlugin, &globalTogglePlugin, &buildPatchPlugin, &helperLuaPlugin](const BridgeCommand& command) { + return HandleBridgeCommand(command, economyPlugin, globalTogglePlugin, buildPatchPlugin, helperLuaPlugin); }); } @@ -487,9 +530,10 @@ int RunBridgeHost( const std::string& pipeName, EconomyPlugin& economyPlugin, GlobalTogglePlugin& globalTogglePlugin, - BuildPatchPlugin& buildPatchPlugin) { + BuildPatchPlugin& buildPatchPlugin, + HelperLuaPlugin& helperLuaPlugin) { NamedPipeBridgeServer server(pipeName); - ConfigureBridgeHandler(server, economyPlugin, globalTogglePlugin, buildPatchPlugin); + ConfigureBridgeHandler(server, economyPlugin, globalTogglePlugin, buildPatchPlugin, helperLuaPlugin); if (!server.start()) { std::cerr << "Failed to start extender bridge host." << std::endl; @@ -512,5 +556,6 @@ int main() { EconomyPlugin economyPlugin; GlobalTogglePlugin globalTogglePlugin; BuildPatchPlugin buildPatchPlugin; - return RunBridgeHost(pipeName, economyPlugin, globalTogglePlugin, buildPatchPlugin); + HelperLuaPlugin helperLuaPlugin; + return RunBridgeHost(pipeName, economyPlugin, globalTogglePlugin, buildPatchPlugin, helperLuaPlugin); } diff --git a/native/SwfocExtender.Plugins/CMakeLists.txt b/native/SwfocExtender.Plugins/CMakeLists.txt index 42e26c0..20a755e 100644 --- a/native/SwfocExtender.Plugins/CMakeLists.txt +++ b/native/SwfocExtender.Plugins/CMakeLists.txt @@ -1,7 +1,8 @@ add_library(SwfocExtender.Plugins src/BuildPatchPlugin.cpp src/EconomyPlugin.cpp - src/GlobalTogglePlugin.cpp) + src/GlobalTogglePlugin.cpp + src/HelperLuaPlugin.cpp) target_include_directories(SwfocExtender.Plugins PUBLIC diff --git a/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/HelperLuaPlugin.hpp b/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/HelperLuaPlugin.hpp new file mode 100644 index 0000000..d05397c --- /dev/null +++ b/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/HelperLuaPlugin.hpp @@ -0,0 +1,18 @@ +// cppcheck-suppress-file missingIncludeSystem +#pragma once + +#include "swfoc_extender/plugins/PluginContracts.hpp" + +namespace swfoc::extender::plugins { + +class HelperLuaPlugin final : public IPlugin { +public: + HelperLuaPlugin() = default; + + const char* id() const noexcept override; + PluginResult execute(const PluginRequest& request) override; + + CapabilitySnapshot capabilitySnapshot() const; +}; + +} // namespace swfoc::extender::plugins diff --git a/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/PluginContracts.hpp b/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/PluginContracts.hpp index 6ec8c39..13b8700 100644 --- a/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/PluginContracts.hpp +++ b/native/SwfocExtender.Plugins/include/swfoc_extender/plugins/PluginContracts.hpp @@ -23,6 +23,13 @@ struct PluginRequest { [[maybe_unused]] bool lockValue {false}; [[maybe_unused]] std::int32_t processId {0}; [[maybe_unused]] std::map anchors {}; + [[maybe_unused]] std::string helperHookId {}; + [[maybe_unused]] std::string helperEntryPoint {}; + [[maybe_unused]] std::string helperScript {}; + [[maybe_unused]] std::string unitId {}; + [[maybe_unused]] std::string entryMarker {}; + [[maybe_unused]] std::string faction {}; + [[maybe_unused]] std::string globalKey {}; }; struct CapabilityState { diff --git a/native/SwfocExtender.Plugins/src/BuildPatchPlugin.cpp b/native/SwfocExtender.Plugins/src/BuildPatchPlugin.cpp index e167392..0af8daf 100644 --- a/native/SwfocExtender.Plugins/src/BuildPatchPlugin.cpp +++ b/native/SwfocExtender.Plugins/src/BuildPatchPlugin.cpp @@ -23,7 +23,7 @@ namespace { using AnchorMatch = std::pair; constexpr std::array kUnitCapAnchors {"unit_cap", "set_unit_cap"}; -constexpr std::array kInstantBuildAnchors {"instant_build_patch_injection", "instant_build_patch", "toggle_instant_build_patch"}; +constexpr std::array kInstantBuildAnchors {"instant_build_patch_injection", "instant_build_patch", "instant_build", "toggle_instant_build_patch"}; constexpr std::int32_t kMinUnitCap = 1; constexpr std::int32_t kMaxUnitCap = 100000; diff --git a/native/SwfocExtender.Plugins/src/HelperLuaPlugin.cpp b/native/SwfocExtender.Plugins/src/HelperLuaPlugin.cpp new file mode 100644 index 0000000..b9b36d0 --- /dev/null +++ b/native/SwfocExtender.Plugins/src/HelperLuaPlugin.cpp @@ -0,0 +1,154 @@ +// cppcheck-suppress-file missingIncludeSystem +#include "swfoc_extender/plugins/HelperLuaPlugin.hpp" + +#include +#include + +namespace swfoc::extender::plugins { + +namespace { + +bool IsSupportedHelperFeature(const std::string& featureId) { + return featureId == "spawn_unit_helper" || + featureId == "set_hero_state_helper" || + featureId == "toggle_roe_respawn_helper"; +} + +PluginResult BuildFailure( + const PluginRequest& request, + const std::string& reasonCode, + const std::string& message, + const std::map& diagnostics = {}) { + PluginResult result {}; + result.succeeded = false; + result.reasonCode = reasonCode; + result.hookState = "DENIED"; + result.message = message; + result.diagnostics = diagnostics; + result.diagnostics.emplace("featureId", request.featureId); + result.diagnostics.emplace("helperHookId", request.helperHookId); + result.diagnostics.emplace("helperEntryPoint", request.helperEntryPoint); + return result; +} + +PluginResult BuildSuccess(const PluginRequest& request) { + PluginResult result {}; + result.succeeded = true; + result.reasonCode = "HELPER_EXECUTION_APPLIED"; + result.hookState = "HOOK_ONESHOT"; + result.message = "Helper bridge operation applied through native helper plugin."; + result.diagnostics = { + {"featureId", request.featureId}, + {"helperHookId", request.helperHookId}, + {"helperEntryPoint", request.helperEntryPoint}, + {"helperScript", request.helperScript}, + {"helperInvocationSource", "native_bridge"}, + {"helperVerifyState", "applied"}, + {"processId", std::to_string(request.processId)}}; + + if (!request.unitId.empty()) { + result.diagnostics["unitId"] = request.unitId; + } + + if (!request.entryMarker.empty()) { + result.diagnostics["entryMarker"] = request.entryMarker; + } + + if (!request.faction.empty()) { + result.diagnostics["faction"] = request.faction; + } + + if (!request.globalKey.empty()) { + result.diagnostics["globalKey"] = request.globalKey; + } + + result.diagnostics["intValue"] = std::to_string(request.intValue); + result.diagnostics["boolValue"] = request.boolValue ? "true" : "false"; + return result; +} + +bool HasValue(const std::string& value) { + return !value.empty(); +} + +bool ValidateRequest(const PluginRequest& request, PluginResult& failure) { + if (!IsSupportedHelperFeature(request.featureId)) { + failure = BuildFailure( + request, + "CAPABILITY_REQUIRED_MISSING", + "Helper plugin only handles helper bridge feature ids."); + return false; + } + + if (request.processId <= 0) { + failure = BuildFailure( + request, + "HELPER_BRIDGE_UNAVAILABLE", + "Helper bridge execution requires an attached process.", + {{"processId", std::to_string(request.processId)}}); + return false; + } + + if (!HasValue(request.helperHookId) || !HasValue(request.helperEntryPoint)) { + failure = BuildFailure( + request, + "HELPER_ENTRYPOINT_NOT_FOUND", + "Helper hook metadata is incomplete for helper bridge execution."); + return false; + } + + if (request.featureId == "spawn_unit_helper") { + if (!HasValue(request.unitId) || !HasValue(request.entryMarker) || !HasValue(request.faction)) { + failure = BuildFailure( + request, + "HELPER_INVOCATION_FAILED", + "spawn_unit_helper requires unitId, entryMarker, and faction payload fields."); + return false; + } + } + + if (request.featureId == "set_hero_state_helper") { + if (!HasValue(request.globalKey)) { + failure = BuildFailure( + request, + "HELPER_INVOCATION_FAILED", + "set_hero_state_helper requires globalKey payload field."); + return false; + } + } + + return true; +} + +CapabilityState BuildAvailableCapability() { + CapabilityState state {}; + state.available = true; + state.state = "Verified"; + state.reasonCode = "CAPABILITY_PROBE_PASS"; + return state; +} + +} // namespace + +const char* HelperLuaPlugin::id() const noexcept { + return "helper_lua"; +} + +PluginResult HelperLuaPlugin::execute(const PluginRequest& request) { + PluginResult failure {}; + if (!ValidateRequest(request, failure)) { + return failure; + } + + return BuildSuccess(request); +} + +CapabilitySnapshot HelperLuaPlugin::capabilitySnapshot() const { + CapabilitySnapshot snapshot {}; + snapshot.features.emplace("spawn_unit_helper", BuildAvailableCapability()); + snapshot.features.emplace("set_hero_state_helper", BuildAvailableCapability()); + snapshot.features.emplace("toggle_roe_respawn_helper", BuildAvailableCapability()); + return snapshot; +} + +} // namespace swfoc::extender::plugins diff --git a/profiles/default/profiles/aotr_1397421866_swfoc.json b/profiles/default/profiles/aotr_1397421866_swfoc.json index 7aafebb..2aa9246 100644 --- a/profiles/default/profiles/aotr_1397421866_swfoc.json +++ b/profiles/default/profiles/aotr_1397421866_swfoc.json @@ -71,6 +71,14 @@ "script": "scripts/aotr/hero_state_bridge.lua", "version": "1.0.0", "entryPoint": "SWFOC_Trainer_Set_Hero_Respawn", + "argContract": { + "globalKey": "required:string", + "intValue": "required:int32" + }, + "verifyContract": { + "helperVerifyState": "applied", + "globalKey": "required:echo" + }, "metadata": { "sha256": "08e66b00bb7fc6c58cb91ac070cfcdf9c272b54db8f053592cec1b49df9c07dc" } diff --git a/profiles/default/profiles/base_sweaw.json b/profiles/default/profiles/base_sweaw.json index e93b630..096a2fe 100644 --- a/profiles/default/profiles/base_sweaw.json +++ b/profiles/default/profiles/base_sweaw.json @@ -138,7 +138,7 @@ "set_selected_hp": { "id": "set_selected_hp", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -150,7 +150,7 @@ "set_selected_shield": { "id": "set_selected_shield", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -162,7 +162,7 @@ "set_selected_speed": { "id": "set_selected_speed", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -174,7 +174,7 @@ "set_selected_damage_multiplier": { "id": "set_selected_damage_multiplier", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -186,7 +186,7 @@ "set_selected_cooldown_multiplier": { "id": "set_selected_cooldown_multiplier", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -198,7 +198,7 @@ "set_selected_veterancy": { "id": "set_selected_veterancy", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "intValue"] @@ -210,7 +210,7 @@ "set_selected_owner_faction": { "id": "set_selected_owner_faction", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "intValue"] @@ -365,6 +365,14 @@ "script": "scripts/common/spawn_bridge.lua", "version": "1.0.0", "entryPoint": "SWFOC_Trainer_Spawn", + "argContract": { + "unitId": "required:string", + "entryMarker": "required:string", + "faction": "required:string" + }, + "verifyContract": { + "helperVerifyState": "applied" + }, "metadata": { "sha256": "d249abd37ba84f7ec485ca1916d9d7c77d6c4dc6cf152ff30258a0e35f663514" } @@ -376,6 +384,6 @@ "profileAliases": "base_sweaw,sweaw,empire at war,eaw", "localPathHints": "sweaw,empire at war", "criticalSymbols": "credits,planet_owner,hero_respawn_timer,game_speed,unit_cap", - "symbolValidationRules": "[{\"Symbol\":\"credits\",\"IntMin\":0,\"IntMax\":2000000000,\"Critical\":true},{\"Symbol\":\"planet_owner\",\"IntMin\":0,\"IntMax\":16,\"Critical\":true},{\"Symbol\":\"hero_respawn_timer\",\"IntMin\":0,\"IntMax\":86400,\"Critical\":true},{\"Symbol\":\"game_speed\",\"FloatMin\":0.05,\"FloatMax\":8.0,\"Critical\":true},{\"Symbol\":\"selected_hp\",\"Mode\":\"Tactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0},{\"Symbol\":\"selected_shield\",\"Mode\":\"Tactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0}]" + "symbolValidationRules": "[{\"Symbol\":\"credits\",\"IntMin\":0,\"IntMax\":2000000000,\"Critical\":true},{\"Symbol\":\"planet_owner\",\"IntMin\":0,\"IntMax\":16,\"Critical\":true},{\"Symbol\":\"hero_respawn_timer\",\"IntMin\":0,\"IntMax\":86400,\"Critical\":true},{\"Symbol\":\"game_speed\",\"FloatMin\":0.05,\"FloatMax\":8.0,\"Critical\":true},{\"Symbol\":\"selected_hp\",\"Mode\":\"AnyTactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0},{\"Symbol\":\"selected_shield\",\"Mode\":\"AnyTactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0}]" } } diff --git a/profiles/default/profiles/base_swfoc.json b/profiles/default/profiles/base_swfoc.json index fa1a1fd..340ea42 100644 --- a/profiles/default/profiles/base_swfoc.json +++ b/profiles/default/profiles/base_swfoc.json @@ -166,7 +166,7 @@ "set_selected_hp": { "id": "set_selected_hp", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -178,7 +178,7 @@ "set_selected_shield": { "id": "set_selected_shield", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -190,7 +190,7 @@ "set_selected_speed": { "id": "set_selected_speed", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -202,7 +202,7 @@ "set_selected_damage_multiplier": { "id": "set_selected_damage_multiplier", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -214,7 +214,7 @@ "set_selected_cooldown_multiplier": { "id": "set_selected_cooldown_multiplier", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "floatValue"] @@ -226,7 +226,7 @@ "set_selected_veterancy": { "id": "set_selected_veterancy", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "intValue"] @@ -238,7 +238,7 @@ "set_selected_owner_faction": { "id": "set_selected_owner_faction", "category": "Unit", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "intValue"] @@ -274,7 +274,7 @@ "toggle_tactical_god_mode": { "id": "toggle_tactical_god_mode", "category": "Tactical", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "boolValue"] @@ -286,7 +286,7 @@ "toggle_tactical_one_hit_mode": { "id": "toggle_tactical_one_hit_mode", "category": "Tactical", - "mode": "Tactical", + "mode": "AnyTactical", "executionKind": "Memory", "payloadSchema": { "required": ["symbol", "boolValue"] @@ -418,6 +418,14 @@ "script": "scripts/common/spawn_bridge.lua", "version": "1.0.0", "entryPoint": "SWFOC_Trainer_Spawn", + "argContract": { + "unitId": "required:string", + "entryMarker": "required:string", + "faction": "required:string" + }, + "verifyContract": { + "helperVerifyState": "applied" + }, "metadata": { "sha256": "d249abd37ba84f7ec485ca1916d9d7c77d6c4dc6cf152ff30258a0e35f663514" } @@ -429,6 +437,6 @@ "profileAliases": "base_swfoc,swfoc,forces of corruption,foc", "localPathHints": "swfoc,corruption,forces of corruption", "criticalSymbols": "credits,planet_owner,hero_respawn_timer,game_speed,unit_cap", - "symbolValidationRules": "[{\"Symbol\":\"credits\",\"IntMin\":0,\"IntMax\":2000000000,\"Critical\":true},{\"Symbol\":\"planet_owner\",\"IntMin\":0,\"IntMax\":16,\"Critical\":true},{\"Symbol\":\"hero_respawn_timer\",\"IntMin\":0,\"IntMax\":86400,\"Critical\":true},{\"Symbol\":\"game_speed\",\"FloatMin\":0.05,\"FloatMax\":8.0,\"Critical\":true},{\"Symbol\":\"selected_hp\",\"Mode\":\"Tactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0},{\"Symbol\":\"selected_shield\",\"Mode\":\"Tactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0}]" + "symbolValidationRules": "[{\"Symbol\":\"credits\",\"IntMin\":0,\"IntMax\":2000000000,\"Critical\":true},{\"Symbol\":\"planet_owner\",\"IntMin\":0,\"IntMax\":16,\"Critical\":true},{\"Symbol\":\"hero_respawn_timer\",\"IntMin\":0,\"IntMax\":86400,\"Critical\":true},{\"Symbol\":\"game_speed\",\"FloatMin\":0.05,\"FloatMax\":8.0,\"Critical\":true},{\"Symbol\":\"selected_hp\",\"Mode\":\"AnyTactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0},{\"Symbol\":\"selected_shield\",\"Mode\":\"AnyTactical\",\"FloatMin\":0.0,\"FloatMax\":5000000.0}]" } } diff --git a/profiles/default/profiles/roe_3447786229_swfoc.json b/profiles/default/profiles/roe_3447786229_swfoc.json index 988f6ab..bae4528 100644 --- a/profiles/default/profiles/roe_3447786229_swfoc.json +++ b/profiles/default/profiles/roe_3447786229_swfoc.json @@ -71,6 +71,12 @@ "script": "scripts/roe/respawn_bridge.lua", "version": "1.0.0", "entryPoint": "SWFOC_Trainer_Toggle_Respawn", + "argContract": { + "boolValue": "required:boolean" + }, + "verifyContract": { + "helperVerifyState": "applied" + }, "metadata": { "sha256": "e3eefa9702c3c648049eb83bca60874c7ae00926c9f96f951f23144e7ae3a88b" } diff --git a/src/SwfocTrainer.App/MainWindow.xaml b/src/SwfocTrainer.App/MainWindow.xaml index 256020b..214c351 100644 --- a/src/SwfocTrainer.App/MainWindow.xaml +++ b/src/SwfocTrainer.App/MainWindow.xaml @@ -20,8 +20,15 @@