From 3840b18fecb41289e9bdfa7017a333a7e38a46ff Mon Sep 17 00:00:00 2001 From: Microck Date: Sat, 10 Oct 2026 10:50:05 +0000 Subject: [PATCH] feat(mod-options): trim akron's native mod options to enable and menu bind Celeste's mod options for Akron now hold only an Enabled switch and the menu bind (keyboard and controller). Enabled is read once at load: when off, Akron installs nothing on the next launch, including error reporting, except its mod options entry and a filter that keeps its [Command] methods out of the debug console. Streamer Mode, Safe Mode, Send diagnostics, the Automatic Error Reports opt-out and the Everest keyboard and controller config screens are removed from this section; the overlay still has the first four. Fast Lookout's hold bind, which starts unbound and was only settable from those Everest screens, gets a Hold row in its overlay popup. --- CHANGELOG.md | 8 +- Source/Commands/akron-qa-commands.cs | 2 +- Source/Module/AkronModule.cs | 46 ++++- Source/Module/AkronModuleSettings.cs | 4 + Source/Module/akron-module-menu.cs | 184 +++++++++++------- Source/Module/akron-module-overlay-input.cs | 51 +++++ Source/Module/akron-module-player-runtime.cs | 2 +- Source/Overlay/akron-diagnostics-menu.cs | 4 +- .../Overlay/akron-overlay-bindable-actions.cs | 2 + Source/Overlay/akron-overlay-bypass-popups.cs | 15 ++ .../Overlay/akron-overlay-runtime-popups.cs | 16 +- docs/feature-guide/interface.mdx | 3 +- docs/feature-guide/player.mdx | 1 + docs/getting-started/first-run.mdx | 2 +- docs/getting-started/hotkeys.mdx | 10 +- docs/player-guide/overlay.mdx | 2 +- docs/reference/error-reporting.mdx | 2 +- docs/troubleshooting/common-issues.mdx | 3 +- tests/module-settings-tests.cs | 34 ++-- tests/overlay-tests.cs | 4 +- 20 files changed, 287 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4324d75..4357e995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ This project uses version tags that match the mod version in `everest.yaml`, whi ### Added -- Add Automatic Error Reports for selected Akron startup, overlay, settings and StartPos failures. Reporting is on by default when a Sentry destination is configured, with an opt-out in Interface and mod options. Existing saved Off choices are preserved. Reports send only bounded Akron stack details and the mod version. Local logging and Send diagnostics keep their existing behavior. +- Add Automatic Error Reports for selected Akron startup, overlay, settings and StartPos failures. Reporting is on by default when a Sentry destination is configured, with an opt-out in the Interface tab. Existing saved Off choices are preserved. Reports send only bounded Akron stack details and the mod version. Local logging and Send diagnostics keep their existing behavior. + +### Changed + +- Trim Akron's entry in Celeste's mod options to an **Enabled** switch and the menu bind. Streamer Mode, Safe Mode, and Send diagnostics stay in the overlay, and action binds are set by right-clicking overlay rows. +- Turning **Enabled** off removes Akron's overlay, HUD, hotkeys, interop, error reports, and debug console commands on the next launch. Only its mod options entry stays, so it can be turned back on there. +- Add a **Hold** bind row to the Fast Lookout popup, set with its **Bind** button. The hold starts unbound, and Everest's keyboard and controller config used to be the only place to set it. Keyboard keys in this hold now act as one chord, like Akron's other binds: a hold saved as two separate keys needs both held, so bind it again if you meant either key. Controller and mouse buttons still work on their own. ## Akron Beta 84 diff --git a/Source/Commands/akron-qa-commands.cs b/Source/Commands/akron-qa-commands.cs index d7aac74f..8023decf 100644 --- a/Source/Commands/akron-qa-commands.cs +++ b/Source/Commands/akron-qa-commands.cs @@ -305,7 +305,7 @@ public static void QaFastLookout(string heldText = "true", string vanillaValueTe float result = AkronModule.ApplyFastLookoutMultiplierForQa(vanillaValue, holdPressed); Log("qa-fast-lookout-enabled: " + AkronModule.Settings.FastLookout.ToString().ToLowerInvariant()); Log("qa-fast-lookout-hold-forced: " + holdPressed.ToString().ToLowerInvariant()); - Log("qa-fast-lookout-live-hold: " + (AkronModule.Settings.FastLookoutHold?.Check ?? false).ToString().ToLowerInvariant()); + Log("qa-fast-lookout-live-hold: " + AkronModule.IsFastLookoutHoldHeld().ToString().ToLowerInvariant()); Log("qa-fast-lookout-patched-constants: " + AkronModule.FastLookoutPatchedConstantCount.ToString(CultureInfo.InvariantCulture)); Log("qa-fast-lookout-multiplier: " + AkronModuleSettings.ClampFastLookoutMultiplier(AkronModule.Settings.FastLookoutMultiplier).ToString(CultureInfo.InvariantCulture)); Log("qa-fast-lookout-vanilla-value: " + vanillaValue.ToString("0.###", CultureInfo.InvariantCulture)); diff --git a/Source/Module/AkronModule.cs b/Source/Module/AkronModule.cs index c53fe10c..f2907bbc 100644 --- a/Source/Module/AkronModule.cs +++ b/Source/Module/AkronModule.cs @@ -91,10 +91,6 @@ internal static bool TryGetPracticeAreaSelectionPreview(Level level, bool isAuto private static ulong renderedStartPosFrameGeneration; private static int freshRoomInitializationUpdateDepth; private static readonly Queue afterEngineUpdateActions = new Queue(); - private static readonly MethodInfo CreateKeyboardConfigUiMethod = - typeof(EverestModule).GetMethod("CreateKeyboardConfigUI", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - private static readonly MethodInfo CreateButtonConfigUiMethod = - typeof(EverestModule).GetMethod("CreateButtonConfigUI", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo PlayerDeadBodyEndMethod = typeof(PlayerDeadBody).GetMethod("End", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly FieldInfo PlayerDeadBodyDeathEffectField = @@ -132,10 +128,37 @@ public AkronModule() { Logger.SetLogLevel(nameof(AkronModule), LogLevel.Info); } + // Settings.Enabled as it was when this session loaded. The switch takes effect on the + // next launch, so this stays fixed even if the player flips the setting meanwhile. + internal static bool EnabledThisSession { get; private set; } + + // Everest rebuilds the command list after modules initialize and again whenever a mod + // registers or unregisters, so filtering each registration is the only removal that + // sticks. + private static void SkipAkronCommandsWhileDisabled(On.Monocle.Commands.orig_ProcessMethod orig, Monocle.Commands self, MethodInfo method) { + if (method.DeclaringType?.Assembly == typeof(AkronModule).Assembly) { + return; + } + + orig(self, method); + } + public override void Load() { + // Normalized even when disabled: the mod options menu still edits the menu bind. + AkronModuleSettings.EnsureCurrentKeybindDefaults(Settings); + EnabledThisSession = Settings.Enabled; + if (!EnabledThisSession) { + // No overlay, HUD, logs, interop or error reports this session. Initialize, + // LoadContent and Unload return early too, so nothing below ever runs half set up. + // The one hook keeps Akron's [Command] methods out of the debug console: Everest + // finds them by reflection whether or not Akron loaded, and they assume a loaded Akron. + On.Monocle.Commands.ProcessMethod += SkipAkronCommandsWhileDisabled; + Logger.Log(LogLevel.Info, nameof(AkronModule), "Akron is disabled in its mod options; nothing loaded this session."); + return; + } + ConfigureErrorReporting(); renderedStartPosFrameGeneration = AkronActions.StartPosFrameGeneration; - AkronModuleSettings.EnsureCurrentKeybindDefaults(Settings); AkronModuleSettings.DropUnkeyedAutomationAreas(Settings); AkronLog.Normal(nameof(AkronModule), "load start; " + AkronLog.DescribeSettings()); AkronAudioSplitter.Load(); @@ -260,6 +283,10 @@ private static MethodInfo ResolveLookoutRoutineMethod() { } public override void Initialize() { + if (!EnabledThisSession) { + return; + } + try { AkronMotionSmoothingInterop.RefreshLoadedState(); AkronMotionSmoothingInterop.ApplyAkronSettings(); @@ -271,6 +298,10 @@ public override void Initialize() { } public override void LoadContent(bool firstLoad) { + if (!EnabledThisSession) { + return; + } + try { AkronImGuiRenderer.WarmUp(); } catch (Exception exception) { @@ -280,6 +311,11 @@ public override void LoadContent(bool firstLoad) { } public override void Unload() { + if (!EnabledThisSession) { + On.Monocle.Commands.ProcessMethod -= SkipAkronCommandsWhileDisabled; + return; + } + AkronTelemetry.Stop(); AkronDiagnosticsMenu.CloseActive(); if (Engine.Instance != null) { diff --git a/Source/Module/AkronModuleSettings.cs b/Source/Module/AkronModuleSettings.cs index a4d13e2e..dac0c3c8 100644 --- a/Source/Module/AkronModuleSettings.cs +++ b/Source/Module/AkronModuleSettings.cs @@ -55,6 +55,10 @@ public partial class AkronModuleSettings : EverestModuleSettings { private bool frameBypassHideStretchedEdges; private bool frameBypassSillyMode; + // Master switch, read once at load (AkronModule.EnabledThisSession). Off means Akron + // installs nothing on the next launch except its mod options entry, which is where it + // can be turned back on. + public bool Enabled { get; set; } = true; public bool StreamerMode { get; set; } // ProofModeOverlay is normally enabled indirectly by Submission Mode. The // direct command path exists for QA and review automation, not as a diff --git a/Source/Module/akron-module-menu.cs b/Source/Module/akron-module-menu.cs index 9d6280f7..88119dd9 100644 --- a/Source/Module/akron-module-menu.cs +++ b/Source/Module/akron-module-menu.cs @@ -3,104 +3,158 @@ using Celeste; using Celeste.Mod; using FMOD.Studio; +using Microsoft.Xna.Framework.Input; using Monocle; namespace Celeste.Mod.Akron; +// Celeste's native mod options hold only what must work without Akron's overlay: the +// master switch, so a disabled Akron can be turned back on, and the menu bind, so a +// conflicting bind can be fixed when the overlay cannot be opened. Everything else +// lives in the overlay. public partial class AkronModule { public override void CreateModMenuSection(TextMenu menu, bool inGame, EventInstance snapshot) { CreateModMenuSectionHeader(menu, inGame, snapshot); - menu.Add(new TextMenu.SubHeader("Overlay", topPadding: false)); - AddWrappedModMenuSubHeaders(menu, "Most Akron options live in the in-game overlay.", topPadding: false); - AddHotkeyPreview(menu, "Open Overlay", Settings.ToggleOverlay); - - menu.Add(new TextMenu.SubHeader("Native Bindings")); - menu.Add(new TextMenu.Button(Dialog.Clean("options_keyconfig")).Pressed(() => OpenKeyboardConfig(menu))); - menu.Add(new TextMenu.Button(Dialog.Clean("options_btnconfig")).Pressed(() => OpenButtonConfig(menu))); - - menu.Add(new TextMenu.SubHeader("Safety")); - menu.Add(new TextMenu.OnOff("Streamer Mode", Settings.StreamerMode).Change(value => Settings.StreamerMode = value)); - menu.Add(new TextMenu.OnOff("Safe Mode", Settings.SafeMode).Change(value => Settings.SafeMode = value)); - - menu.Add(new TextMenu.SubHeader("Support")); - AddWrappedModMenuSubHeaders(menu, AkronTelemetry.IsConfigured - ? "Automatic error reports are on by default. Turn off below. Reports send Akron stack frames and its version to Sentry at " + AkronTelemetry.Destination + ". No logs, exception messages, names, save contents or full paths are included." - : "Automatic error reports are not configured in this build."); - menu.Add(new TextMenu.OnOff("Automatic Error Reports", AkronTelemetry.IsConfigured && Settings.ErrorReportingEnabled) { - Disabled = !AkronTelemetry.IsConfigured - }.Change(SetErrorReportingEnabled)); - menu.Add(new TextMenu.Button("Send diagnostics").Pressed(() => AkronDiagnosticsMenu.Open(menu))); + TextMenu.SubHeader restartNote = new TextMenu.SubHeader("Restart Celeste to apply.", topPadding: false) { + Visible = Settings.Enabled != EnabledThisSession + }; + menu.Add(new TextMenu.OnOff("Enabled", Settings.Enabled).Change(value => { + Settings.Enabled = value; + restartNote.Visible = value != EnabledThisSession; + })); + menu.Add(restartNote); + + menu.Add(new TextMenu.Button("Menu Bind (Keyboard)").Pressed(() => OpenMenuBindConfig(menu, new MenuBindKeyboardConfigUI()))); + menu.Add(new TextMenu.Button("Menu Bind (Controller)").Pressed(() => OpenMenuBindConfig(menu, new MenuBindButtonConfigUI()))); } - private void OpenKeyboardConfig(TextMenu menu) { + private static void OpenMenuBindConfig(TextMenu menu, TextMenu config) { if (Engine.Scene == null) { return; } menu.Focused = false; - KeyboardConfigUI keyboardConfig = CreateKeyboardConfigUiMethod != null - ? (KeyboardConfigUI) CreateKeyboardConfigUiMethod.Invoke(this, new object[] { menu }) - : new ModuleSettingsKeyboardConfigUI(this); - keyboardConfig.OnClose = () => { menu.Focused = true; }; - Engine.Scene.Add(keyboardConfig); + // In a level, keep a remapped key from toggling the HUD while this screen is open, + // as Everest's ModuleSettings*ConfigUI constructors do. These screens derive from + // Celeste's base config UI and skip those constructors. + Level level = Engine.Scene as Level; + bool allowHudHide = level?.AllowHudHide ?? false; + if (level != null) { + level.AllowHudHide = false; + } + config.OnClose = () => { + if (level != null) { + level.AllowHudHide = allowHudHide; + } + menu.Focused = true; + }; + Engine.Scene.Add(config); Engine.Scene.OnEndOfFrame += () => Engine.Scene.Entities.UpdateLists(); } - private void OpenButtonConfig(TextMenu menu) { - if (Engine.Scene == null) { + // Akron reads several keys in the menu bind as a chord (all held together), while + // Celeste's remap adds each new key next to the old ones. Keeping only the key just + // added makes this screen replace the bind instead of building a chord. Anything else + // (a key removed, or nothing pressed) leaves the list alone, so a chord set in the + // overlay survives opening this screen. + // + // Celeste appends the new key and, at Input.MaxBindings, trims the oldest key in the + // same update, so an add shows up as a longer list or as a new last key at the same + // length. Comparing count and last key catches both without copying the list. + internal static void KeepOnlyNewKey(List keys, int countBefore, Keys lastBefore) { + if (keys.Count < 2) { return; } - menu.Focused = false; - ButtonConfigUI buttonConfig = CreateButtonConfigUiMethod != null - ? (ButtonConfigUI) CreateButtonConfigUiMethod.Invoke(this, new object[] { menu }) - : new ModuleSettingsButtonConfigUI(this); - buttonConfig.OnClose = () => { menu.Focused = true; }; - Engine.Scene.Add(buttonConfig); - Engine.Scene.OnEndOfFrame += () => Engine.Scene.Entities.UpdateLists(); - } + Keys last = keys[keys.Count - 1]; + bool added = keys.Count > countBefore || (keys.Count == countBefore && last != lastBefore); + if (!added) { + return; + } - private static void AddHotkeyPreview(TextMenu menu, string label, ButtonBinding binding) { - menu.Add(new TextMenu.Button(label + ": " + AkronModuleSettings.DescribeBinding(binding)) { Selectable = false }); + keys.Clear(); + keys.Add(last); } - private static void AddWrappedModMenuSubHeaders(TextMenu menu, string text, bool topPadding = true) { - foreach (string line in WrapModMenuLine(text)) { - menu.Add(new TextMenu.SubHeader(line, topPadding)); - topPadding = false; + // Index of the bind row in both screens below: header, input info, then the row. + private const int MenuBindRowIndex = 2; + + private sealed class MenuBindKeyboardConfigUI : KeyboardConfigUI { + // Everest's patched base constructor calls Reload() without an index, which leaves + // the selection on a row that no longer exists. Select the bind row, as Everest's + // own ModuleSettingsKeyboardConfigUI does. + public MenuBindKeyboardConfigUI() { + Reload(MenuBindRowIndex); } - } - internal static IReadOnlyList WrapModMenuLine(string text, int maxCharacters = 64) { - List lines = new List(); - foreach (string rawParagraph in (text ?? string.Empty).Split('\n')) { - string paragraph = rawParagraph.Trim(); - if (paragraph.Length == 0) { - continue; + public override void Reload(int index = -1) { + Clear(); + Add(new Header(Dialog.Clean("KEY_CONFIG_TITLE"))); + Add(new InputMappingInfo(controllerMode: false)); + AddMapForceLabel("Open Akron", Settings.ToggleOverlay.Binding); + Add(new SubHeader("")); + Add(new Button(Dialog.Clean("KEY_CONFIG_RESET")) { + IncludeWidthInMeasurement = false, + AlwaysCenter = true, + OnPressed = ResetPressed + }); + if (index >= 0) { + Selection = index; } + } - string current = string.Empty; - foreach (string word in paragraph.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { - if (current.Length == 0) { - current = word; - continue; - } + public override void Update() { + List keys = Settings.ToggleOverlay.Binding.Keyboard; + int countBefore = keys.Count; + Keys lastBefore = countBefore > 0 ? keys[countBefore - 1] : Keys.None; + base.Update(); + KeepOnlyNewKey(keys, countBefore, lastBefore); + } - if (current.Length + 1 + word.Length > maxCharacters) { - lines.Add(current); - current = word; - continue; - } + public override void Reset() { + Binding binding = Settings.ToggleOverlay.Binding; + binding.Keyboard.Clear(); + binding.Mouse.Clear(); + binding.Keyboard.Add(Keys.Tab); + Input.Initialize(); + Reload(Selection); + } + } - current += " " + word; - } + // Controller buttons in the menu bind are alternatives in both Akron and Celeste, so + // Celeste's remap behavior is kept as is. + private sealed class MenuBindButtonConfigUI : ButtonConfigUI { + public MenuBindButtonConfigUI() { + // Celeste's screen only listens for its own gameplay buttons. Everest's mod + // binding screen adds these four so mod binds can use them; do the same. + All.Add(Buttons.Back); + All.Add(Buttons.BigButton); + All.Add(Buttons.RightStick); + All.Add(Buttons.LeftStick); + Reload(MenuBindRowIndex); + } - if (current.Length > 0) { - lines.Add(current); + public override void Reload(int index = -1) { + Clear(); + Add(new Header(Dialog.Clean("BTN_CONFIG_TITLE"))); + Add(new InputMappingInfo(controllerMode: true)); + AddMapForceLabel("Open Akron", Settings.ToggleOverlay.Binding); + Add(new SubHeader("")); + Add(new Button(Dialog.Clean("KEY_CONFIG_RESET")) { + IncludeWidthInMeasurement = false, + AlwaysCenter = true, + OnPressed = ResetPressed + }); + if (index >= 0) { + Selection = index; } } - return lines.Count == 0 ? new[] { string.Empty } : lines; + public override void Reset() { + Settings.ToggleOverlay.Binding.Controller.Clear(); + Input.Initialize(); + Reload(Selection); + } } } diff --git a/Source/Module/akron-module-overlay-input.cs b/Source/Module/akron-module-overlay-input.cs index b24c40a6..fd2fb58c 100644 --- a/Source/Module/akron-module-overlay-input.cs +++ b/Source/Module/akron-module-overlay-input.cs @@ -725,6 +725,57 @@ private static bool IsMouseButtonPressed(MInput.MouseData.MouseButtons button) { }; } + // Fast Lookout's hold: the keyboard keys are one chord (overlay capture stores Ctrl+V as + // two keys, which ButtonBinding.Check would treat as alternatives), while controller and + // mouse buttons stay alternatives as they were under Check. IsButtonBindingHeld is not + // used because it reads controller buttons as a chord too and ignores the mouse, which + // would break holds saved before the overlay row existed. + internal static bool IsFastLookoutHoldHeld() { + ButtonBinding binding = Settings.FastLookoutHold; + if (binding == null) { + return false; + } + + if (TryGetButtonBindingKeys(binding, out IReadOnlyCollection keys) && + IsKeyboardBindingHeld(keys, Keyboard.GetState())) { + return true; + } + + if (TryGetButtonBindingButtons(binding, out IReadOnlyCollection buttons) && + buttons != null && + Input.Gamepad >= 0 && + Input.Gamepad < MInput.GamePads.Length) { + foreach (Buttons button in buttons) { + if (button != 0 && MInput.GamePads[Input.Gamepad].CurrentState.IsButtonDown(button)) { + return true; + } + } + } + + if (TryGetButtonBindingMouseButtons(binding, out IReadOnlyCollection mouseButtons) && + mouseButtons != null) { + foreach (MInput.MouseData.MouseButtons mouseButton in mouseButtons) { + if (IsMouseButtonHeld(mouseButton)) { + return true; + } + } + } + + return false; + } + + private static bool IsMouseButtonHeld(MInput.MouseData.MouseButtons button) { + MouseState current = MInput.Mouse.CurrentState; + return button switch { + MInput.MouseData.MouseButtons.Left => current.LeftButton == ButtonState.Pressed, + MInput.MouseData.MouseButtons.Right => current.RightButton == ButtonState.Pressed, + MInput.MouseData.MouseButtons.Middle => current.MiddleButton == ButtonState.Pressed, + MInput.MouseData.MouseButtons.XButton1 => current.XButton1 == ButtonState.Pressed, + MInput.MouseData.MouseButtons.XButton2 => current.XButton2 == ButtonState.Pressed, + _ => false + }; + } + private static ButtonBinding GetStartPosSlotBinding(int slot) { return slot switch { 1 => Settings.LoadStartPosSlot1, diff --git a/Source/Module/akron-module-player-runtime.cs b/Source/Module/akron-module-player-runtime.cs index caca20bf..a8bfd31b 100644 --- a/Source/Module/akron-module-player-runtime.cs +++ b/Source/Module/akron-module-player-runtime.cs @@ -811,7 +811,7 @@ private static void LookoutRoutineIlHook(ILContext il) { } private static float ApplyFastLookoutMultiplier(float vanillaValue) { - return ApplyFastLookoutMultiplier(vanillaValue, Settings.FastLookoutHold?.Check == true); + return ApplyFastLookoutMultiplier(vanillaValue, IsFastLookoutHoldHeld()); } private static float ApplyFastLookoutMultiplier(float vanillaValue, bool holdPressed) { diff --git a/Source/Overlay/akron-diagnostics-menu.cs b/Source/Overlay/akron-diagnostics-menu.cs index 5f3ae0cd..304a7b8b 100644 --- a/Source/Overlay/akron-diagnostics-menu.cs +++ b/Source/Overlay/akron-diagnostics-menu.cs @@ -38,13 +38,13 @@ internal static class AkronDiagnosticsMenu { internal static bool IsOpen => ownerScene != null; internal static bool AcceptsTextInput => IsOpen && !showStatus && !emptyConfirmationActive && endpointError == null; - internal static void Open(TextMenu parentMenu = null) { + internal static void Open() { Scene scene = Engine.Scene; if (scene == null || IsOpen) return; // Release the hide-pause cache before capturing the parent's visibility. if (scene is Level) AkronRuntimeOptions.RestorePauseMenuVisibility(); ownerScene = scene; - parent = parentMenu ?? scene.Entities.OfType().FirstOrDefault(menu => menu.Focused); + parent = scene.Entities.OfType().FirstOrDefault(menu => menu.Focused); if (parent != null) { parentWasVisible = parent.Visible; parentWasFocused = parent.Focused; diff --git a/Source/Overlay/akron-overlay-bindable-actions.cs b/Source/Overlay/akron-overlay-bindable-actions.cs index 145d757e..2519ba79 100644 --- a/Source/Overlay/akron-overlay-bindable-actions.cs +++ b/Source/Overlay/akron-overlay-bindable-actions.cs @@ -1001,7 +1001,9 @@ internal static bool NativeBindingMatchesPressedKey( internal static bool UsesKeyboardChordDispatch( AkronModuleSettings settings, ButtonBinding binding) { + // FastLookoutHold is held as a keyboard chord too (AkronModule.IsFastLookoutHoldHeld). return ReferenceEquals(binding, settings.ToggleOverlay) || + ReferenceEquals(binding, settings.FastLookoutHold) || ReferenceEquals(binding, settings.SetStartPos) || ReferenceEquals(binding, settings.LoadStartPos) || ReferenceEquals(binding, settings.ClearStartPos) || diff --git a/Source/Overlay/akron-overlay-bypass-popups.cs b/Source/Overlay/akron-overlay-bypass-popups.cs index 6167d1ad..1a3b83d3 100644 --- a/Source/Overlay/akron-overlay-bypass-popups.cs +++ b/Source/Overlay/akron-overlay-bypass-popups.cs @@ -167,6 +167,21 @@ private void DrawFastLookoutPopupControls(string popupId) 10, popupId, "Lookout camera speed multiplier while Fast Lookout is enabled."); + + // Fast Lookout only speeds up while this is held, and it starts unbound, so this row + // is the only in-game way to make the feature do anything. + ImGui.Separator(); + DrawCursorHoldBindingRow( + "Player/Fast Lookout Hold", + "Fast Lookout / Hold", + "fast-lookout", + AkronModule.Settings.FastLookoutHold, + value => AkronModule.Settings.FastLookoutHold = value, + AkronModuleSettings.CreateEmptyButtonBinding(), + popupId, + "Hold this while using a lookout to move the camera faster.", + rowLabel: "Hold", + tooltipTitle: "Fast Lookout hold"); } private void DrawNoDeathWipePopupControls(string popupId) diff --git a/Source/Overlay/akron-overlay-runtime-popups.cs b/Source/Overlay/akron-overlay-runtime-popups.cs index a2d71f2e..47189d5c 100644 --- a/Source/Overlay/akron-overlay-runtime-popups.cs +++ b/Source/Overlay/akron-overlay-runtime-popups.cs @@ -215,6 +215,8 @@ private void DrawEntityInspectorReportPlacementRows(string popupId) { true); } + // Bind row for a held ButtonBinding. Cursor tools use the default labels; other holds, + // like Fast Lookout's, pass their own. private void DrawCursorHoldBindingRow( string actionKey, string displayName, @@ -223,13 +225,15 @@ private void DrawCursorHoldBindingRow( Action setter, ButtonBinding defaultBinding, string popupId, - string tooltip) { + string tooltip, + string rowLabel = "Cursor", + string tooltipTitle = "Cursor hold") { const float bindingButtonWidth = 172f; const float actionButtonWidth = 54f; float labelWidth = CalculatePopupLabelWidth(bindingButtonWidth); string bindingText = AkronModuleSettings.DescribeBinding(binding); - DrawPopupRowLabel("Cursor", labelWidth); + DrawPopupRowLabel(rowLabel, labelWidth); ImGui.PushStyleColor(ImGuiCol.Button, AkronImGuiTheme.FrameBackground); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AkronImGuiTheme.ButtonHovered); ImGui.PushStyleColor(ImGuiCol.ButtonActive, AkronImGuiTheme.ButtonActive); @@ -237,28 +241,28 @@ private void DrawCursorHoldBindingRow( StartButtonBindingCapture(actionKey, displayName, setter, clearMenuBinding: false); } ImGui.PopStyleColor(3); - DrawPopupTooltip(tooltip, "Cursor hold"); + DrawPopupTooltip(tooltip, tooltipTitle); DrawPopupRowLabel("", labelWidth); float spacing = ImGui.GetStyle().ItemSpacing.X; if (ImGui.Button("Bind##" + idPrefix + "-cursor-bind-" + popupId, new NumericsVector2(actionButtonWidth, 0f))) { StartButtonBindingCapture(actionKey, displayName, setter, clearMenuBinding: false); } - DrawPopupTooltip("Capture a keyboard chord or controller button.", "Cursor hold"); + DrawPopupTooltip("Capture a keyboard chord or controller button.", tooltipTitle); ImGui.SameLine(0f, spacing); if (ImGui.Button("Clear##" + idPrefix + "-cursor-clear-" + popupId, new NumericsVector2(actionButtonWidth, 0f))) { setter(AkronModuleSettings.CreateEmptyButtonBinding()); menuBindingRevision++; } - DrawPopupTooltip("Clear this binding.", "Cursor hold"); + DrawPopupTooltip("Clear this binding.", tooltipTitle); ImGui.SameLine(0f, spacing); if (ImGui.Button("Default##" + idPrefix + "-cursor-default-" + popupId, new NumericsVector2(actionButtonWidth + 10f, 0f))) { setter(defaultBinding); menuBindingRevision++; } - DrawPopupTooltip("Restore Akron's default binding.", "Cursor hold"); + DrawPopupTooltip("Restore Akron's default binding.", tooltipTitle); } private void DrawHitboxTrailPopupControls(string popupId) { diff --git a/docs/feature-guide/interface.mdx b/docs/feature-guide/interface.mdx index df91ebb7..09ac5400 100644 --- a/docs/feature-guide/interface.mdx +++ b/docs/feature-guide/interface.mdx @@ -98,8 +98,7 @@ Keybind, HUD, audio, and recorder packs are not map catalog entries. Post those Text input is active only while a diagnostics field has focus. Closing the form releases it so Speedrun Tool hotkeys work again. -**Send diagnostics** opens an Akron overlay form before any upload. It is also -available in Akron's Everest mod options. Add a title, describe what happened, +**Send diagnostics** opens an Akron overlay form before any upload. Add a title, describe what happened, list steps to reproduce it, and add the expected behavior or other context. Choose **Send now**. You can leave every field blank, but Akron asks you to confirm before sending. Reports use the configured community upload service, diff --git a/docs/feature-guide/player.mdx b/docs/feature-guide/player.mdx index e078a162..83d525b7 100644 --- a/docs/feature-guide/player.mdx +++ b/docs/feature-guide/player.mdx @@ -97,6 +97,7 @@ Rows with a triangle expose the controls listed below. | Dash Number | Text | Dash number text color. | Cheat | | Dash Number | Outline | Dash number outline color. | Cheat | | Fast Lookout | Multiplier | Set lookout camera speed while the hold bind is pressed. | Cheat | +| Fast Lookout | Hold | Bind the key or button that speeds up the lookout camera. It starts unbound, so Fast Lookout does nothing until this is set. | Cheat | | Golden Transparency | Opacity | Set golden berry and follower opacity percentage used while Golden Transparency is toggled on. | Normal clear | | Golden Transparency | Reset opacity | Restore the default 55% golden transparency opacity. | Normal clear | | Grab Mode | Hold | Vanilla grab behavior: hold the grab key to grab. | Normal clear | diff --git a/docs/getting-started/first-run.mdx b/docs/getting-started/first-run.mdx index 076b7117..ab5dd30b 100644 --- a/docs/getting-started/first-run.mdx +++ b/docs/getting-started/first-run.mdx @@ -10,7 +10,7 @@ Follow this tutorial after installing Akron. Open the overlay, confirm Akron is Start Celeste through Everest with Akron enabled. - Press `Tab`, the default overlay bind. If this bind was changed or conflicts with another mod, configure it in Everest's mod options. + Press `Tab`, the default overlay bind. If this bind was changed or conflicts with another mod, change it with **Menu Bind** in Akron's mod options. Akron shows the current attempt status: Unclassified, Goldberry/Hardlist clear, Normal clear, or Cheat. Fresh attempts start Unclassified until Akron's policy hooks record a classification. The status can escalate during an attempt, but it does not downgrade. diff --git a/docs/getting-started/hotkeys.mdx b/docs/getting-started/hotkeys.mdx index 27764ba8..2c639cd8 100644 --- a/docs/getting-started/hotkeys.mdx +++ b/docs/getting-started/hotkeys.mdx @@ -3,7 +3,7 @@ title: Hotkeys description: Default keyboard and controller bindings for Akron actions, with overlay bind configuration, input formats, and recovery steps. --- -Akron uses Everest's keyboard and controller configuration for binds. Use these configuration screens to ensure bindings remain consistent with your modded Celeste setup. +Bind Akron actions from the overlay: right-click an action row, and review current binds in the **Keybinds** tab. The bind that opens the menu can also be changed from Akron's entry in Celeste's mod options, so a conflicting menu bind can be fixed without opening the overlay. ## Default keybindings @@ -56,11 +56,11 @@ Actions marked `-` have no default binding on that input type. Open Menu starts ## Configure a bind - - From Celeste's mod options, open the Everest keyboard or controller configuration screen. - - Search or scroll to the Akron action you want to change, such as the overlay toggle, retry, StartPos set/load, freeze, hitboxes, or inspector actions. + Open Akron's overlay and search or scroll to the action row you want to bind, such as retry, StartPos set/load, freeze, hitboxes, or inspector actions. If the menu bind itself is the problem, use **Menu Bind (Keyboard)** or **Menu Bind (Controller)** in Akron's mod options instead. + + + Right-click the row. Bind the key or controller input. Avoid using inputs already mapped to Celeste movement, pause, confirm, or other critical mod actions. diff --git a/docs/player-guide/overlay.mdx b/docs/player-guide/overlay.mdx index 9ded0b70..51559223 100644 --- a/docs/player-guide/overlay.mdx +++ b/docs/player-guide/overlay.mdx @@ -43,7 +43,7 @@ Hover a row to see its classification in the tooltip, and hover a popup option f - Check the visible attempt status and policy indicators before enabling features. - Keep attempt-changing tools in explicit room-lab or testing setups, not submitted-run setups. -- Use Everest's Akron mod options for Safe Mode or per-map compatibility overrides. These settings are separate from attempt status. +- Use the overlay's Safe Mode row or per-map compatibility overrides. These settings are separate from attempt status. - Use search to select the exact row and avoid adjacent settings. - Export the current setup before importing shared packs or making broad changes. diff --git a/docs/reference/error-reporting.mdx b/docs/reference/error-reporting.mdx index 0d5e5629..0759f6c8 100644 --- a/docs/reference/error-reporting.mdx +++ b/docs/reference/error-reporting.mdx @@ -5,7 +5,7 @@ description: Choose whether Akron sends selected errors to Sentry and configure ## Player choice -**Automatic Error Reports** is on by default when a valid Sentry destination is configured. Turn it off in the Interface tab or Akron's mod options under Support. Mod options show the configured destination. The control is unavailable when no valid destination is configured, and no reports are sent. +**Automatic Error Reports** is on by default when a valid Sentry destination is configured. Turn it off in the overlay's Interface tab. Turning Akron off in its mod options also stops reports from the next launch, since a disabled Akron does not start reporting. The control is unavailable when no valid destination is configured, and no reports are sent. Reporting does not show a launch notice. Handled errors keep their existing player-facing behavior; automatic reporting does not add an error popup. diff --git a/docs/troubleshooting/common-issues.mdx b/docs/troubleshooting/common-issues.mdx index 7e972118..897ad152 100644 --- a/docs/troubleshooting/common-issues.mdx +++ b/docs/troubleshooting/common-issues.mdx @@ -85,8 +85,7 @@ Those are separate operations and may have different causes. ## Send diagnostics to a maintainer -Open **Interface → Send diagnostics** in Akron's overlay, or **Send diagnostics** -in Akron's Everest mod options. The Akron form has fields for a title, what +Open **Interface → Send diagnostics** in Akron's overlay. The Akron form has fields for a title, what happened, steps to reproduce it, expected behavior, and extra context. The combined report accepts up to 4,000 characters. Choose **Send now** to upload. If every field is blank, Akron asks you to confirm first. Opening the form does diff --git a/tests/module-settings-tests.cs b/tests/module-settings-tests.cs index 58b6a2bf..aa08c444 100644 --- a/tests/module-settings-tests.cs +++ b/tests/module-settings-tests.cs @@ -1035,20 +1035,26 @@ public void PauseTimerFreezeOnlyReleasesStopsOwnedByAkron(bool stoppedByAkron, b Assert.Equal(expected, AkronModule.ShouldReleasePauseTimerFreezeStop(stoppedByAkron, freezeTimerEnabled, canFreezeTimer, freezeTimerDuringPause)); } - [Fact] - public void ModMenuDescriptionsWrapBeforeTheyCanWidenTheMenu() - { - IReadOnlyList lines = AkronModule.WrapModMenuLine( - "Akron menu rows are bindable in the overlay by right-clicking or Shift-clicking them. These native config UIs remain for built-in keyboard/controller bindings.", - maxCharacters: 48); - - Assert.All(lines, line => Assert.InRange(line.Length, 1, 48)); - Assert.Equal(new[] { - "Akron menu rows are bindable in the overlay by", - "right-clicking or Shift-clicking them. These", - "native config UIs remain for built-in", - "keyboard/controller bindings." - }, lines); + [Theory] + // Celeste's keyboard remap appends the pressed key; the menu bind keeps only that key, + // because Akron reads several keys as a chord. + [InlineData(new[] { Keys.Tab, Keys.F1 }, 1, Keys.Tab, new[] { Keys.F1 })] + [InlineData(new Keys[] { Keys.F1 }, 0, Keys.None, new[] { Keys.F1 })] + // At Input.MaxBindings Celeste trims the oldest key in the same update, so the add + // keeps the length and only the last key changes. + [InlineData(new[] { Keys.B, Keys.C, Keys.D, Keys.E, Keys.F, Keys.G, Keys.H, Keys.F6 }, 8, Keys.H, new[] { Keys.F6 })] + // Removing a key, or no remap at all, leaves the list alone, including a chord set in + // the overlay. + [InlineData(new Keys[] { }, 1, Keys.Tab, new Keys[] { })] + [InlineData(new[] { Keys.LeftControl, Keys.Tab }, 2, Keys.Tab, new[] { Keys.LeftControl, Keys.Tab })] + [InlineData(new[] { Keys.LeftControl, Keys.Tab }, 3, Keys.F1, new[] { Keys.LeftControl, Keys.Tab })] + public void MenuBindKeyboardScreenReplacesTheKey(Keys[] afterRemap, int countBefore, Keys lastBefore, Keys[] expected) + { + List keys = new List(afterRemap); + + AkronModule.KeepOnlyNewKey(keys, countBefore, lastBefore); + + Assert.Equal(expected, keys); } [Theory] diff --git a/tests/overlay-tests.cs b/tests/overlay-tests.cs index 6d7968a4..4a71df16 100644 --- a/tests/overlay-tests.cs +++ b/tests/overlay-tests.cs @@ -593,6 +593,9 @@ public void NativeChordActionsRequireTheirModifiersBeforeClaimingPeriod() { Assert.True(AkronOverlay.UsesKeyboardChordDispatch( settings, settings.ToggleOverlay)); + Assert.True(AkronOverlay.UsesKeyboardChordDispatch( + settings, + settings.FastLookoutHold)); Assert.False(AkronOverlay.UsesKeyboardChordDispatch( settings, settings.Retry)); @@ -1889,7 +1892,6 @@ public void EntityInspectorRowUsesSettingForActiveColorAndPopupBindings() { Assert.Contains("AkronModule.Settings.EntityInspectorCursorHold", entityInspectorPopup); Assert.Contains("AkronModule.Settings.EntityInspectorPinHoverPreview", entityInspectorPopup); Assert.Contains("\"Hover preview\"", entityInspectorPopup); - Assert.Contains("DrawPopupRowLabel(\"Cursor\"", popupSource); Assert.Contains("DrawEntityInspectorReportPlacementRows(popupId)", entityInspectorPopup); Assert.Contains("EntityInspectorPinPlacement", popupSource); Assert.Contains("EntityInspectorPinShowPropertiesByDefault", popupSource);