Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Source/Commands/akron-qa-commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
46 changes: 41 additions & 5 deletions Source/Module/AkronModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,6 @@ internal static bool TryGetPracticeAreaSelectionPreview(Level level, bool isAuto
private static ulong renderedStartPosFrameGeneration;
private static int freshRoomInitializationUpdateDepth;
private static readonly Queue<Action> afterEngineUpdateActions = new Queue<Action>();
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 =
Expand Down Expand Up @@ -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;
Comment thread
Microck marked this conversation as resolved.
}

ConfigureErrorReporting();
renderedStartPosFrameGeneration = AkronActions.StartPosFrameGeneration;
AkronModuleSettings.EnsureCurrentKeybindDefaults(Settings);
AkronModuleSettings.DropUnkeyedAutomationAreas(Settings);
AkronLog.Normal(nameof(AkronModule), "load start; " + AkronLog.DescribeSettings());
AkronAudioSplitter.Load();
Expand Down Expand Up @@ -260,6 +283,10 @@ private static MethodInfo ResolveLookoutRoutineMethod() {
}

public override void Initialize() {
if (!EnabledThisSession) {
return;
}

try {
AkronMotionSmoothingInterop.RefreshLoadedState();
AkronMotionSmoothingInterop.ApplyAkronSettings();
Expand All @@ -271,6 +298,10 @@ public override void Initialize() {
}

public override void LoadContent(bool firstLoad) {
if (!EnabledThisSession) {
return;
}

try {
AkronImGuiRenderer.WarmUp();
} catch (Exception exception) {
Expand All @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions Source/Module/AkronModuleSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
184 changes: 119 additions & 65 deletions Source/Module/akron-module-menu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())));
Comment thread
Microck marked this conversation as resolved.
Comment thread
Microck marked this conversation as resolved.
}

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> 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);
Comment thread
Microck marked this conversation as resolved.
}
}

internal static IReadOnlyList<string> WrapModMenuLine(string text, int maxCharacters = 64) {
List<string> lines = new List<string>();
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> 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);
Comment thread
Microck marked this conversation as resolved.
}

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);
}
}
}
Loading
Loading