From 98754ec044887081e8188d8b0602db5b7e26732a Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 31 Aug 2026 04:12:45 +0900 Subject: [PATCH 1/5] Set transform touch option default enabled --- src/frontend/qt_sdl/Config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index affa8e57d3..c6809188d9 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -265,7 +265,7 @@ namespace Config {"Instance*.Metroid.Enable.stylusMode", false}, {"Instance*.Metroid.Enable.topScreenTouch", false}, {"Instance*.Metroid.Enable.touchScreenAimOnly", false}, - {"Instance*.Metroid.Enable.touchScreenAimOnlySuspendForTransform", false}, + {"Instance*.Metroid.Enable.touchScreenAimOnlySuspendForTransform", true}, {"Instance*.Metroid.Enable.stylusHideCursorInGame", false}, {"Instance*.Metroid.Enable.stylusConfineCursorToTopScreen", false}, {"Instance*.Metroid.Enable.stylusHoldCursorAtCenterWhenNotClicking", false}, From 69617a4c51569ebb632f1224092959f1597bec68 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 31 Aug 2026 12:16:03 +0900 Subject: [PATCH 2/5] docs: refresh MelonPrime feature references --- docs/architecture/gameplay/patch-system.md | 4 +- docs/development/hud/custom-hud-runtime.md | 12 +- docs/development/ui/settings-and-edit-mode.md | 106 ++++- docs/features/README.md | 2 + docs/features/hud/README.md | 1 + docs/features/hud/custom-hud-enemy-target.md | 2 +- docs/features/hud/custom-hud-scoreboard.md | 171 +++++++ docs/features/hud/custom-hud-settings.md | 2 +- docs/features/input/zoom-input-methods.md | 434 ++++++++---------- docs/features/melonprime-settings.md | 8 +- docs/features/melonprime-settings/README.md | 4 +- .../input-compatibility.md | 48 +- .../melonprime-settings/input-methods.md | 61 ++- .../melonprime-settings/menu-language.md | 13 +- .../melonprime-settings/morph-ball-boost.md | 6 +- .../melonprime-settings/online-enemy-hp.md | 20 +- .../melonprime-settings/section-disclosure.md | 23 +- .../touch-screen-aim-only.md | 164 +++++++ docs/features/rendering/README.md | 1 + docs/features/rendering/dx12-backend.md | 4 + .../rendering/high-resolution-coordinates.md | 134 ++++++ docs/features/rendering/vulkan-backend.md | 9 +- 22 files changed, 877 insertions(+), 352 deletions(-) create mode 100644 docs/features/hud/custom-hud-scoreboard.md create mode 100644 docs/features/melonprime-settings/touch-screen-aim-only.md create mode 100644 docs/features/rendering/high-resolution-coordinates.md diff --git a/docs/architecture/gameplay/patch-system.md b/docs/architecture/gameplay/patch-system.md index 12566eb5ed..514b7f1f4e 100644 --- a/docs/architecture/gameplay/patch-system.md +++ b/docs/architecture/gameplay/patch-system.md @@ -417,10 +417,10 @@ WeaponSwitch, TransformGate, NativeAimDelta, etc.) are documented in the | Use firmware language | `MelonPrimePatchUseFirmwareLanguage.*` | Registry: `OutOfGameFrame` | Adventure-aware; applied in menus; adapter passes `rom.isInAdventure` as 4th arg | | FPS Camera Lock | `MelonPrimePatchFpsCameraLock.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | Public independent camera-behavior patch; distinct from the `LowLatencyMode` ImmediateSync/MoonLike instruction hook. | | Show headshot online | `MelonPrimePatchShowHeadshotOnline.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | | -| Show enemy HP meter online | `MelonPrimePatchShowEnemyHpMeterOnline.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | | +| Show enemy target information online | `MelonPrimePatchShowEnemyHpMeterOnline.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | Legacy key/source name says HP meter; current UI exposes the broader target-information path. | | Disable double-damage multiplier | `MelonPrimePatchDisableDoubleDamageMultiplier.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | Pairs with Damage-Notify-Purple | | No picking up specific items | `MelonPrimePatchNoPickingUpSpecificItems.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | | -| Touch-screen aim only (battle) | `MelonPrimePatchTouchScreenAimOnly.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | Neutralizes the three in-match bottom-screen HUD hit-tests (Morph Ball, weapon quick slots, weapon menu) with `mov r0,#0` so the whole touch screen stays aim input; double-tap jump and the touch boost gesture are left alone | +| Touch-screen aim only (battle) | `MelonPrimePatchTouchScreenAimOnly.*` | Registry: `BattleRuntime \| ConfigReload` (`RF_OnLeave \| RF_OnStop`) | Requires Stylus Mode plus the parent setting; neutralizes three HUD hit-tests with `mov r0,#0`. Standard transform can temporarily restore/reapply them when `Metroid.Enable.touchScreenAimOnlySuspendForTransform` is enabled. See [Touch-Screen-Aim-Only](../../features/melonprime-settings/touch-screen-aim-only.md). | --- diff --git a/docs/development/hud/custom-hud-runtime.md b/docs/development/hud/custom-hud-runtime.md index d98ce145f6..36c915b477 100644 --- a/docs/development/hud/custom-hud-runtime.md +++ b/docs/development/hud/custom-hud-runtime.md @@ -154,6 +154,9 @@ The always-on scoreboard still follows the regular HUD gameplay-state timing: it is not tied to the native START/TAB scoreboard, and it is not forced on by the HP gauge's individual Show setting. Its independent Show setting controls only whether the scoreboard is drawn during normal gameplay. +The focused [Custom HUD scoreboard](../../features/hud/custom-hud-scoreboard.md) +page documents physical-slot versus result-order ownership and the four color +variants loaded for each hunter. Current cached data inside `MelonPrimeHudRender.cpp` includes: - weapon icon images @@ -321,7 +324,7 @@ top-screen BG1-3 layers and flash the native visor. Fix (host-side, selective): | OPT-HRT1 | HUD runtime state is cached by `NDS* + MainRAM + NumFrames + player offset + ROM group`; base/adventure/visible reads are cached eagerly, while ammo, owned weapons, bomb count, match status, rank, and time values are cached lazily only when their HUD elements need them | Repeated 120/240Hz draws of the same emulated frame reuse gameplay HUD state instead of re-reading RAM, while disabled elements do not populate unused cache fields | Per game frame | | OPT-SCB1 | Scoreboard match metadata (Hunter ID, Hunter Rank/Stars, decoded player name, active roster, TeamIndex, game mode, and team flags) is cached once after match join and keyed by NDS/RAM/ROM group plus match serial; only live metrics and `ResultSlots` are read per emulated frame | Removes repeated static scoreboard RAM reads and name decoding from the per-frame path while preserving live score, standing, and display-order updates | Once per match / dynamic fields per game frame | | OPT-SCB2 | Scoreboard/rank `QFont` and `QFontMetrics` values are cached by HUD config epoch, base-font generation/size, and HUD scale | Removes scoreboard font-size and metrics reconstruction from the high-refresh per-frame draw path while keeping settings and scale changes immediate | On relevant change | -| OPT-SCB3 | `ScoreboardRenderPlan` retains final localized/elided strings, text advances, cell geometry, colors, icon rectangles, and rank/metric roles. A compact `ScoreboardStructureKey` compares only match serial, mode/roster/order/team structure, visibility, and active slots; live cells use a semantic value key, so time changes are bucketed to the displayed second | Removes per-frame name/string comparison, repeated localization/elision, and full-layout rebuilding. A changed dynamic cell formats and measures once, then updates in place when it fits; overflow falls back to a structural rebuild | Structural change / changed dynamic cells per game frame | +| OPT-SCB3 | `ScoreboardRenderPlan` retains final localized/elided strings, text advances, cell geometry, colors, icon rectangles, hunter color variant, and rank/metric roles. A compact `ScoreboardStructureKey` compares match serial, mode/roster/order/team structure, visibility, result slots, and each physical slot's active state, team, and Hunter ID; live cells use a semantic value key, so time changes are bucketed to the displayed second | Removes per-frame name/string comparison, repeated localization/elision, and full-layout rebuilding while rebuilding when icon identity/variant ownership can change. A changed dynamic cell formats and measures once, then updates in place when it fits; overflow falls back to a structural rebuild | Structural change / changed dynamic cells per game frame | | OPT-SCB4 | Scoreboard outline paths are stored on each retained text cell. Outline OFF bypasses path creation and lookup entirely; outline ON lazily builds one path per cell and clears it only when the displayed text/font plan changes | Removes the old 35-slot raster-cache machinery and avoids outline work when the feature is disabled while retaining the existing drawText behavior | Cell text/font change / outline-enabled draw | | OPT-VFR1 | Software, OpenGL, Vulkan, DX12, and Metal presentation paths first probe only NDS identity plus emulated game frame. A new frame renders and commits the already-probed identity with its stamp after rendering; only a same-frame candidate constructs and compares the extended stamp containing config/font/state generations, menu language, output size, transform/origin, renderer generation, HUD toggle, and edit mode | Avoids extended-stamp pre-render validation and duplicate game-frame probing on new emulated frames while retaining exact same-frame image/texture reuse and invalidation. A rendered frame still commits one stamp after rendering for later same-frame validation | Per presentation callback | | OPT-PERF1 | Developer-only HUD phase probe reports `HudStateRead`, scoreboard plan/raster, other painter, clear, hash, upload preparation, GPU upload, composite, and total-active timing with calls/sum/avg/p50/p95/max plus visual render/reuse, identity-probe, stamp-check/commit, plan rebuild, structure-check, dynamic-cell, time-visual-change, outline-path, hash, and upload counters | Makes 60/120/144/240 Hz high-refresh regressions measurable without per-frame log spam | Aggregate once per second when `MELONPRIME_PERF=1` | @@ -332,9 +335,10 @@ The scoreboard keeps the existing cache boundaries: match-static values (Hunter Stars/rank, license name, active roster, `TeamIndex`, mode, and team flags) are read after match join, while time/kills/deaths/points/standings and `ResultSlots` remain live per emulated frame. `ScoreboardRenderPlan` stores the final display strings and -device-independent geometry. `ScoreboardStructureKey` deliberately excludes names, -Hunter ID, Stars, and live metrics; the match serial is the invalidation boundary for -those match-static values. A live value update first compares a semantic value key +device-independent geometry. `ScoreboardStructureKey` excludes names, Stars, and +live metrics, but includes each slot's Hunter ID and active state because either can +change the selected p1-p4 hunter portrait. The match serial remains the invalidation +boundary for the other match-static values. A live value update first compares a semantic value key (time is represented by visible seconds, including the `MAX` sentinel), then replaces only the affected rank or metric cell. The changed-cell path calls formatting and `horizontalAdvance()` once; a width overflow falls back to a full structural plan diff --git a/docs/development/ui/settings-and-edit-mode.md b/docs/development/ui/settings-and-edit-mode.md index fd5e8b3185..7fe5ea5594 100644 --- a/docs/development/ui/settings-and-edit-mode.md +++ b/docs/development/ui/settings-and-edit-mode.md @@ -48,8 +48,8 @@ After schema changes, run `tools/ci/audits/audit-hud-key-parity.ps1` and | `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfigCustomHudCode.inc` | Custom HUD TOML export/import tab (include fragment) | | `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfigPreview.cpp` | live preview rendering, preview apply flow, snapshot/restore | | `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfigInternal.h` | shared UI helpers, presets, color-sync helpers | -| `src/frontend/qt_sdl/MelonPrimeLocalization.h` | localization API (`Tr`, `LocalizeWidgetTree`, ...) — declarations only | -| `src/frontend/qt_sdl/MelonPrimeLocalization.cpp` | English/Japanese translation tables (`kTranslations`, `kObjectTextTranslations`) | +| `src/frontend/qt_sdl/MelonPrimeLocalization.h` | localization API (`Tr`, `LocalizeWidgetTree`, language selection) | +| `src/frontend/qt_sdl/MelonPrimeLocalization/` | language registry, translation catalog, dynamic text, widget localizer, splash text, and per-domain translation includes | ### Tab structure @@ -62,37 +62,97 @@ After schema changes, run `tools/ci/audits/audit-hud-key-parity.ps1` and | Custom HUD Input/Output | `tabCustomHudCode` | TOML export/import of all Custom HUD settings (auto-discovers widgets in `tabCrosshair`) | ### Constructor/setup flow + Current constructor flow in `MelonPrimeInputConfig.cpp` is: -1. `MelonPrime::UiText::SetMenuLanguageMode(...)` + `setupMenuLanguageControl()` -2. `setupKeyBindings()` -3. `setupSensitivityAndToggles()` - calls `buildSettingBindings()` internally (non-HUD load/save binding table) -4. `setupInputMethodSection()` -5. `setupCollapsibleSections()` -6. `setupCustomHudWidgets()` - programmatic creation of all HUD parameter widgets -7. `setupPreviewConnections()` -8. `setupCustomHudCode()` -9. `MelonPrime::UiText::LocalizeWidgetTree(this)` -10. `snapshotVisualConfig()` + +1. `MelonPrime::UiText::SetMenuLanguageSelection(...)` and + `setupMenuLanguageControl()`; +2. `setupKeyBindings()`; +3. `setupSensitivityAndToggles()` — this calls `buildSettingBindings()` for + non-HUD load/save ownership; +4. `setupInputMethodSection()`; +5. `setupSettingsOrganization()` — moves existing widgets into their current + task-oriented sections and reorders the Settings page; +6. `setupCollapsibleSections()`; +7. `setupCustomHudWidgets()`; +8. `setupPreviewConnections()`; +9. `setupCustomHudCode()`; +10. `MelonPrime::UiText::LocalizeWidgetTree(this)`; +11. `refreshSettingsPresentation()`; +12. platform-specific video/sync presentation; +13. `snapshotVisualConfig()`; +14. wheel guards and the Settings viewport resize event filter. This ordering matters because preview wiring assumes widgets are already initialized. ### Localization -MelonPrime settings labels support English and Japanese through the `MelonPrime::UiText` API -(`MelonPrimeLocalization.h`); the translation tables live in `MelonPrimeLocalization.cpp`. + +MelonPrime settings labels use the registered-language catalog behind the +`MelonPrime::UiText` API. `MelonPrimeLocalization.cpp` is only the unity entry; +the registry, catalogs, localizer, and domain-specific translation includes +live under `src/frontend/qt_sdl/MelonPrimeLocalization/`. Current rules: -- OS locale decides the UI language with `QLocale::system().language()`. -- On Japanese OS, the Settings tab shows a `Menu Language` selector (`Metroid.UI.MenuLanguage`, `0=Japanese`, `1=English`), defaulting to Japanese. -- On non-Japanese OS, the selector is hidden and MelonPrime UI text stays English regardless of the saved language value. +- `Metroid.UI.MenuLanguage=-1` selects the system/default language. +- The Menu Language selector is always visible and contains System default plus + every ID returned by `AllSelectableMenuLanguages()`; it is not limited to + English/Japanese or gated by a Japanese OS locale. +- The language row has a minimum width of 260 px and maximum width of 420 px so + long native language names remain usable without forcing the Settings page + horizontally wider. - English source strings remain the default and are the lookup keys for short text in `MelonPrime::UiText::kTranslations`. -- Long or HTML-rich description labels should use stable object names and Japanese entries in `MelonPrime::UiText::kObjectTextTranslations`. -- Japanese strings are hand-authored; do not use machine-translated text. +- Long or HTML-rich description labels should use stable object names and + explicit entries in the object-text catalog for every registered language. +- Translations are authored entries; do not rely on fallback for a requested + all-language UI change. - Config keys, TOML keys, object names, and enum/storage values remain English and must not be translated. - Static Qt Designer text and generated widget text are localized by `MelonPrime::UiText::LocalizeWidgetTree(this)` after all setup is complete. - Dynamic labels, toggle text, dialog titles, status messages, and in-game overlay text should call `MelonPrime::UiText::Tr(...)` at the point where the text is assigned or drawn. - The in-game DS-space overlay is tight; prefer concise Japanese labels and update text measurement code when changing drawn sample text. -When adding or renaming visible MelonPrime UI text, update both the English call site and the Japanese entry in `MelonPrimeLocalization.cpp`. For C++-created description labels, set a stable object name before `LocalizeWidgetTree(this)` runs. The main call sites are `MelonPrimeInputConfig.cpp`, `MelonPrimeInputConfigCustomHudCode.inc`, `MelonPrimeHudConfigOnScreenEdit.cpp`, and the edit-mode `.inc` fragments (`Defs`, `Draw`, `Input`). +When adding or renaming visible MelonPrime UI text, update the English call site +and every registered language in the relevant translation include. For +C++-created description labels, set a stable object name before +`LocalizeWidgetTree(this)` runs. The main call sites are +`MelonPrimeInputConfig.cpp`, `MelonPrimeInputConfigCustomHudCode.inc`, +`MelonPrimeHudConfigOnScreenEdit.cpp`, and the edit-mode `.inc` fragments. + +### Settings tab organization and responsive presentation + +`setupSettingsOrganization()` presents the Settings page in this visible +order, independent of the historical order in the `.ui` XML: + +1. Language +2. Sensitivity +3. Input Settings +4. Input Method +5. Gameplay Toggles +6. Game Feature Improvements +7. Disable Features +8. Low HP Warning +9. In-Game Apply +10. In-Game Aspect Ratio +11. Screen Sync +12. Cursor Clip Settings +13. Video Quality +14. Volume +15. License Apply +16. Bug Fixes +17. Developer Only + +Touch/stylus controls, Touch-Screen-Aim-Only, top-screen touch, stylus cursor +policy, Morph Ball Boost swipe controls, and Joy2Key compatibility are grouped +under Input Settings. Sensitivity now contains aim tuning only. Moving a widget +does not change its object name, binding, config key, or parent/child state +logic. + +`refreshSettingsPresentation()` disables the horizontal scrollbar and wraps +every full checkbox caption with `QTextLayout` when the viewport size or menu +language changes. The unwrapped text remains in a widget property and in +`accessibleName`, so wrapping is presentation-only and localization can safely +replace the full caption. Wheel events over a non-focused combo box/spin box +scroll the containing Settings page instead of accidentally changing the +control value; focused controls retain their normal wheel behaviour. ### Collapsible sections The settings UI uses toggle buttons and section widgets wired up in `setupCollapsibleSections()`. Toggle state is persisted under `Metroid.UI.Section*` config keys. @@ -179,6 +239,8 @@ It currently saves: Important side effects in `saveConfig()`: - if `Metroid.Visual.ClipCursorToBottomScreenWhenNotInGame` changed, windows schedule `panel->updateClipIfNeeded()` via `QTimer::singleShot` - if `Metroid.Visual.InGameTopScreenOnly` changed, windows schedule `onScreenLayoutChanged()` via Qt queued connection +- stylus cursor visibility/confinement/centre-pin settings refresh the active + panel cursor policy through `refreshStylusCursorSettings()` - `MelonPrime::CustomHud_InvalidateConfigCache(core->HudConfigState())` is called at the end so the per-instance runtime HUD reads updated values on the next frame HUD settings are saved from both the settings dialog (`saveConfig()`) and the in-game edit mode (`CustomHud_ExitEditMode(hudConfig, true, cfg)` -> `Config::Save()`). Both write the same config keys. When the dialog reopens, `setupCustomHudWidgets()` reads the latest config values, ensuring sync. @@ -187,6 +249,10 @@ HUD settings are saved from both the settings dialog (`saveConfig()`) and the in The **Reset sensitivity values** button resets every bound setting whose widget currently belongs to `sectionSensitivity`. It does not maintain a second list of numeric defaults. Instead, it resolves values from `Config.cpp` through the `Config::Table::GetDefaultInt/GetDefaultBool/GetDefaultDouble/GetDefaultString` accessors. `CheckBoolInverted` rows invert the positive stored default when updating a negative **Disable...** checkbox. +Morph Ball Boost controls no longer belong to `sectionSensitivity`, so this +button intentionally does not reset them. Reset those Input Settings values by +editing them explicitly or through a broader configuration reset. + `Low-Latency Aim Mode` remains outside the generic binding table because its save path uses combo item data and a public/developer-mode gate; the reset handler restores it explicitly through the same compiled integer default. The handler blocks widget signals while applying defaults and does not call `SetInt`, `SetBool`, `SetDouble`, `Config::Save()`, or runtime notification functions. The new values are committed only by the dialog's existing Save/OK path. Cancelling the dialog therefore preserves the previously saved sensitivity configuration. diff --git a/docs/features/README.md b/docs/features/README.md index f8e2b5e132..7612850e30 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -29,10 +29,12 @@ the complete route rather than one isolated behavior. - [Custom HUD adventure camera scenes](hud/custom-hud-adventure-camera-scene.md) - [Custom HUD adventure Scan Visor](hud/custom-hud-adventure-scan-visor.md) - [Custom HUD Enemy Target](hud/custom-hud-enemy-target.md) +- [Custom HUD scoreboard](hud/custom-hud-scoreboard.md) - [Zoom input methods](input/zoom-input-methods.md) - [macOS compute renderer restriction](rendering/macos-compute-renderer-restriction.md) - [Vulkan backend](rendering/vulkan-backend.md) - [DirectX 12 backend](rendering/dx12-backend.md) +- [High-resolution vertex coordinates](rendering/high-resolution-coordinates.md) Completed feature/refactor evidence is indexed under [docs/archive/features/](../archive/features/). diff --git a/docs/features/hud/README.md b/docs/features/hud/README.md index 175d087984..11bd3aca53 100644 --- a/docs/features/hud/README.md +++ b/docs/features/hud/README.md @@ -20,6 +20,7 @@ reference. - [Adventure camera scenes](custom-hud-adventure-camera-scene.md) - [Adventure Scan Visor](custom-hud-adventure-scan-visor.md) - [Enemy Target](custom-hud-enemy-target.md) +- [Scoreboard and per-player hunter colors](custom-hud-scoreboard.md) - [Helmet spawn flash](custom-hud-helmet-spawn-flash.md) The focused pages own behavior that needs its own runtime or rendering diff --git a/docs/features/hud/custom-hud-enemy-target.md b/docs/features/hud/custom-hud-enemy-target.md index 43324c157f..6fe83816b4 100644 --- a/docs/features/hud/custom-hud-enemy-target.md +++ b/docs/features/hud/custom-hud-enemy-target.md @@ -8,7 +8,7 @@ Settings: **HUD ENEMY TARGET** section in the Custom HUD dialog, and element **E the in-game layout editor. Keys are `Metroid.Visual.HudEnemyTarget*`; `HudEnemyTargetShow` defaults to `true`. -The dialog section opens with a note row pointing at **Show Enemy HP Meter Online** +The dialog section opens with a note row pointing at **Show Enemy Target Information Online** (`Metroid.GameFeature.ShowEnemyHpMeterOnline`, under GAME FEATURE IMPROVEMENTS), which online play additionally needs — see [Nintendo WFC](#nintendo-wfc). It is a wrapped `HWType::Label` row, not a tooltip, so it is readable without hovering. Nothing is disabled when that fix is diff --git a/docs/features/hud/custom-hud-scoreboard.md b/docs/features/hud/custom-hud-scoreboard.md new file mode 100644 index 0000000000..1658f90634 --- /dev/null +++ b/docs/features/hud/custom-hud-scoreboard.md @@ -0,0 +1,171 @@ +# Custom HUD scoreboard + +## Scope + +The Custom HUD scoreboard is a host-rendered composite built from the running +match state. It is not the ROM's native START/TAB scoreboard and does not patch +the guest scoreboard renderer. This page owns behaviour that cannot be inferred +from the generated property list: roster sampling, result order, team rows, +hunter icon variants, retained-plan invalidation, and runtime boundaries. + +For every user-editable key, default, type, and surface, use the +[generated HUD property schema](../../generated/hud/MelonPrimeHudPropSchemaPhase2a.md). +For preview/import/edit workflow, use the +[Custom HUD settings guide](custom-hud-settings.md). + +## Visibility and setting boundary + +`Metroid.Visual.HudScoreboardShow` controls whether the composite is eligible +to draw. The scoreboard remains subject to the normal Custom HUD enable state, +gameplay-state timing, match availability, and its own visibility snapshot. +It can be configured as an always-on gameplay element; that does not mean +“force the native scoreboard open”. + +Position, panel dimensions, row sizing, colors, typography, outline, icon +sizes, and displayed cells are the `Metroid.Visual.HudScoreboard*` family. +Those properties are schema-owned. The dialog preview is representative only; +the runtime uses live player, team, mode, rank, score, and result-order data. + +## Runtime data model + +`SampleScoreboardSnapshot()` separates physical player slots from display +order: + +```text +playersBySlot[0..3] physical roster records and live values +resultSlots[0..3] slot IDs in the order the game currently displays them +``` + +Each player snapshot includes whether the slot is active, hunter ID, team, +standing, team standing, score/metric data, rank/stars, and decoded name. +`resultSlots` may change as standings change. It never renumbers the physical +slot or changes which duplicate-hunter color belongs to that player. + +Match-static data is cached after match join and keyed by NDS/RAM/ROM identity +plus match serial. Live metrics and `resultSlots` are refreshed per emulated +frame. Match leave, stop, identity change, or a new match serial invalidates +the old snapshot. + +## Hunter icon and color selection + +There are 28 scoreboard portraits: seven hunters times four in-game player +color variants. + +| Hunter ID | Hunter | Resource variants | +| ---: | --- | --- | +| 0 | Samus | p1, p2, p3, p4 | +| 1 | Kanden | p1, p2, p3, p4 | +| 2 | Trace | p1, p2, p3, p4 | +| 3 | Sylux | p1, p2, p3, p4 | +| 4 | Noxus | p1, p2, p3, p4 | +| 5 | Spire | p1, p2, p3, p4 | +| 6 | Weavel | p1, p2, p3, p4 | + +Assets live under `res/assets/scoreboard/hunter_icons/` and are registered as +`:/mph-scoreboard-hunter--p` in `res/melon.qrc`. +`MelonPrimeHudRenderAssets.inc` scales them to the configured hunter-icon +height and stores them in `scoreboardHunterIcons[7][4]`. + +`ResolveScoreboardHunterVariant(snapshot, slot)` follows physical active-slot +order: + +```text +variant = number of lower active slots using the same hunter +variant = min(variant, 3) +``` + +Thus the first active Samus slot gets p1, the next active Samus slot gets p2, +and so on. Different hunters each begin at p1. Invalid/inactive slot or hunter +data safely resolves to variant zero before the later validity check decides +whether an image can be used. + +### Why result order must not select the variant + +Suppose physical slots 0 and 3 both use Trace. Slot 0 is p1 and slot 3 is p2. +If slot 3 moves above slot 0 in `resultSlots`, their rows swap, but their icon +colors do not. Counting duplicates in display order would make colors change +whenever standings change, contradicting the game's player-position ownership. + +Team grouping also does not change the rule. Team rows and result order decide +where a player is drawn; `playersBySlot` decides who the player is and which +duplicate-hunter variant belongs to that slot. + +## Retained render plan + +The scoreboard uses a retained `ScoreboardRenderPlan` rather than formatting, +measuring, and laying out every string on every host frame. Its structural key +contains: + +- match serial, game mode, active-player count, team flags, team mode, and + scoreboard visibility; +- current `resultSlots`; +- each physical slot's team index, hunter ID, and active flag. + +Hunter ID and active state are structural because changing either can change +the icon and the duplicate-hunter variant of later slots. A planned player row +stores both `hunterId` and the resolved `hunterVariant`; drawing then indexes +`scoreboardHunterIcons[hunterId][hunterVariant]` without recomputing ownership. + +Dynamic score/time/standing cells use semantic value keys and can update in +place when the formatted value still fits. Name decoding and other match-static +reads remain outside the high-refresh draw loop. Font/metrics and outline paths +have separate caches so config, scale, or text changes invalidate only the +state that depends on them. + +## Settings dialog and in-game editor + +The Custom HUD tab's `ScoreboardPreviewWidget` uses a synthetic four-player +snapshot and the same broad layout rules. It confirms colors, sizing, spacing, +font, and panel presentation but cannot prove runtime address sampling or live +result ordering. + +In-game edit mode exposes the scoreboard as element index 14, default anchor +3 (middle-left). Moving/resizing/editing writes the same schema-owned keys as +the settings dialog. The preview/editor do not create an independent scoreboard +configuration namespace. + +## Source ownership + +| Source | Responsibility | +| --- | --- | +| `MelonPrimeHudRuntimeSample.inc` | match cache, slot records, live metrics, `resultSlots` | +| `MelonPrimeHudRenderDraw.inc` | variant resolver, row construction, structural key, retained plan, draw | +| `MelonPrimeHudRenderPlan.inc` | structure/render-plan types | +| `MelonPrimeHudRenderAssets.inc` | 7x4 hunter image loading/scaling | +| `MelonPrimeHudRenderConfig.inc` | per-instance image/cache ownership | +| `MelonPrimeHudPropSchema.inc` | user-editable scoreboard property contract | +| `InputConfig/MelonPrimeInputConfigHudPreviews.inc` | dialog preview | +| `res/melon.qrc` | resource aliases | + +## Reverse-engineering references without duplication + +The current host rendering contract above is owned by melonPrimeDS source. +Guest scoreboard geometry and source-field investigations remain in mphCodex: + +- `C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\Scoreboard\Custom-HUD\0203BD14-scoreboard-hunter-icon-bootstrap-JP1_0.md` +- `C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\Scoreboard\Match-Scoreboard-Native-Geometry-AllVersions-JP1_0.md` +- `C:\Users\Admin\Documents\git\mphCodex\mnt\data\analysis\mphAnalysis\topics\gameplay\Match\current\Battle-Runtime-Result-Struct.md` + +These links provide evidence and native-game context rather than a second copy +of this implementation document. If paths move in mphCodex, locate the same +filename; do not silently paste an old investigation into this repository. + +## Verification checklist + +- One instance of each hunter uses p1. +- Two to four active physical slots with the same hunter receive p1 through p4 + in ascending slot order. +- Reorder `resultSlots` by changing standings; row order changes while icon + variants remain attached to physical slots. +- Mix duplicate hunters across teams and verify team grouping does not reassign + variants. +- Mark a lower duplicate slot inactive and verify later variants compact based + on active slots only. +- Exercise hunter ID/active/roster changes and confirm the retained structure + rebuilds instead of drawing a stale portrait. +- Test solo/team modes, ties, result transitions, match leave/rejoin, and a new + match without restarting the emulator. +- Compare dialog preview, in-game edit mode, and runtime at multiple HUD scales. +- Confirm missing/invalid images fail safely and never index outside `[7][4]`. +- Treat static source review and preview checks separately from an actual + multiplayer runtime test. diff --git a/docs/features/hud/custom-hud-settings.md b/docs/features/hud/custom-hud-settings.md index 12ffb48313..becd608659 100644 --- a/docs/features/hud/custom-hud-settings.md +++ b/docs/features/hud/custom-hud-settings.md @@ -67,7 +67,7 @@ persisted separately from the section's visual properties. | CROSSHAIR | Main crosshair, zoom crosshair, inner lines, outer lines, dot, outline, and pixel-position behavior | Crosshair | | HP / AMMO | HP and ammo labels, gauges, outlines, weapon icon, color ramps, and weapon inventory | HP/ammo | | MATCH STATUS HUD | Score, rank, time, bomb count, labels, icons, and their outlines | Match status | -| HUD SCOREBOARD | Scoreboard position, dimensions, rows, colors, and typography | Scoreboard | +| HUD SCOREBOARD | Scoreboard position, dimensions, rows, colors, typography, and slot-stable hunter portraits; see [focused behaviour](custom-hud-scoreboard.md) | Scoreboard | | HUD ENEMY TARGET | Target visibility, layout, HP mode, text, icon, and outlines | Enemy target | | HUD RADAR | Radar position, size, opacity, frame, and outline | Radar | | IN-GAME OSD COLOR | Global OSD color plus message/slot-specific color overrides | None | diff --git a/docs/features/input/zoom-input-methods.md b/docs/features/input/zoom-input-methods.md index 29d6563fb7..0ba6f1457f 100644 --- a/docs/features/input/zoom-input-methods.md +++ b/docs/features/input/zoom-input-methods.md @@ -1,284 +1,242 @@ -# MelonPrimeDS Zoom Input Methods +# Zoom input methods -## Overview +## What this selector changes -MelonPrimeDS currently has three selectable zoom input methods. +The Zoom selector chooses how a host Zoom pressed edge reaches Metroid Prime +Hunters. It does not change zoom sensitivity; that is the separate +`Metroid.Aim.ZoomScale.*` setting family. -| UI state | Config value | Runtime mode | Behavior | +| UI choice | `Metroid.Input.ZoomMethod` | Delivery path | Public build | | --- | ---: | --- | --- | -| all zoom boxes unchecked | `0` | Legacy | Press the zoom button the player's control preset binds. | -| — | `1` | retired | Retired value. Reading it behaves as `0`, and the next save normalises it away. | -| `Use New Method 2 for Zoom` checked | `2` | New2 | Toggle native zoom state by calling `SetPlayerScopeZoom(player, enabled)` through an ARM9 trampoline at the weapon action update. | -| `Use New Method 3 for Zoom` checked | `3` | New3 | Call the same setter from the game's own player input update, after applying the bottom-screen touch shortcut's state and weapon gates. | +| Standard Method | `0` | press the current control preset's Zoom binding | yes; default | +| retired value | `1` | treated as Standard and normalised on save | not displayed | +| New Method 2 | `2` | call `SetPlayerScopeZoom` at the weapon-action hook | no; developer-only | +| New Method 3 | `3` | DirectInvocation call from player input update | no; developer-only | -`Use New Method 2 for Zoom` and `Use New Method 3 for Zoom` are mutually exclusive checkboxes. If both are off, the saved method is Legacy. +The three displayed choices form an exactly-one checkbox group. Selecting one +clears the other two, and attempting to clear the last selection restores it. +There is therefore no supported “all boxes unchecked” state. In a non-developer +build only Standard is available and the runtime ignores native values left by +a developer build. -Relevant config: +`Config.cpp` owns the default (`0`) and range (`0..3`). +`MelonPrimeRuntimeConfig.cpp` resolves the saved value into two hot flags: -```cpp -Metroid.Input.ZoomMethod -0 = Legacy -1 = retired (behaves as 0) -2 = New native toggle -3 = New native 3 (DirectInvocation) -``` - -The default is `0` and the config range is `0..3`. - -New3 is part of the shared DirectInvocation dispatcher; its gates, addresses and lifecycle are -documented with the other Method 2/3 paths in -[input-methods.md](../melonprime-settings/input-methods.md). - -## Runtime Flags - -`ReloadConfigFlags()` is the lifecycle wrapper that loads/clamps the runtime snapshot and applies -it to hot runtime state; that apply step maps `Metroid.Input.ZoomMethod` to the hot booleans. - -```cpp -m_enableNewZoomInputMethod = (ZoomMethod == 1); -m_enableNativeZoomToggle = (ZoomMethod == 2); +```text +value 0 or 1 -> neither native flag +value 2 -> nativeZoomToggle +value 3 -> directInvocationZoom ``` -Legacy is the state where both of those booleans are false. +Disabling a native method clears its pending call and pressed-edge latch so an +old edge cannot be replayed after changing methods. -When New2 is disabled, any native zoom pending call and previous button-down latch are cleared so a mode switch cannot consume stale input. +## Shared host input flow -## Shared Input Flow - -Zoom is projected from the host hotkey state into `IB_ZOOM` in `ProjectDownState()`. - -`ProcessMoveAndButtonsFastImpl()` deliberately only merges `B` and `L`. It does not merge `R` anymore, because zoom can be preset-dependent and is applied separately in `ApplyZoomBindingInput()`. - -Normal per-frame flow: +The configured host key becomes `IB_ZOOM` in `ProjectDownState()`. Zoom is then +handled separately from the fast movement/B/L merge: ```text UpdateInputState() - -> ProjectDownState() sets IB_ZOOM + -> ProjectDownState(): host key -> IB_ZOOM ProcessMoveAndButtonsFastFromReset() - -> movement + B/L + -> movement plus ordinary B/L state ApplyZoomBindingInput() - -> Legacy/New/New2 zoom behavior -``` - -The same zoom application is also used by the re-entrant frame-advance path. - -`ApplyZoomBindingInput()` opens with a host-only gate: with the native zoom toggle off and -`IB_ZOOM` released, every branch below it is a no-op, so it returns before the alt-form check -(`IsPlayerAltForm()`, a MainRAM dereference). The native toggle keeps the gate open even on -released frames because `UpdateNativeZoomToggleInput()` needs them for the release edge and the -zoom-capability auto-disable. Ordering only — no behavior change. - -## Legacy Method - -Legacy mode keeps the old fixed-button behavior. - -```cpp -zoomMask = 1 << INPUT_R; -m_inputMaskFast &= ~zoomMask; + -> alt-form handling + -> spawn-window fallback + -> selected Standard/New2/New3 path ``` -`m_inputMaskFast` is DS input state where cleared bits mean pressed. Holding the external zoom button therefore holds DS `R`. - -If `ImmediateInputEdgeOverlay` is enabled, the overlay also treats zoom as fixed `INPUT_R` in Legacy mode. - -## New Method - -New Method still acts like normal input, but it reads the game's current control-preset binding instead of assuming `R`. - -`ApplyZoomBindingInput()` starts with fixed `R`, then replaces it with the in-game zoom binding when possible. - -```cpp -zoomMask = 1 << INPUT_R; -if (m_enableNewZoomInputMethod && inGame) { - zoomMask = Read16(player + 0x3E0) & 0x0FFF; -} -m_inputMaskFast &= ~zoomMask; -``` +The re-entrant frame-advance path calls the same zoom application routine. Both +native methods keep receiving released frames because they need release-edge +tracking and zoom-capability cleanup; Standard can return immediately when the +host key is up. -This makes Touch R/L and Dual R/L presets work with their own zoom binding. If the binding read fails or returns `0`, it falls back to fixed `R`. +## Alt-Form is Morph Ball Boost, not scope zoom -If `ImmediateInputEdgeOverlay` is enabled, it uses the same idea and reads `player+0x3E0` for the zoom mask before writing the game input struct. +Before selecting a zoom method, `ApplyZoomBindingInput()` checks the local +player's form. In Alt-Form, the host Zoom control drives the control preset's +`MorphBoost` binding and bypasses all scope-zoom methods. The native edge latch +is still synchronised so holding Zoom while leaving Alt-Form does not create a +late scope toggle. -## ImmediateInputEdgeOverlay Interaction +This distinction is important for Dual-style presets: neither Zoom nor Morph +Ball Boost should be described as a fixed DS `R` key. Both use the binding +snapshot resolved from the active in-game control preset. -`MelonPrimePatchImmediateInputEdgeOverlay.inc` is a side-effect ARM9 hook. It does not redirect PC. It runs at the action consumer entry, after the game has polled input and before Fire / Jump / Zoom / Movement handlers consume the input struct. +## Standard Method -The hook reads the current player's binding fields: - -| Offset | Action | -| ---: | --- | -| `+0x368` | move left | -| `+0x36C` | move right | -| `+0x370` | move forward | -| `+0x374` | move backward | -| `+0x398` | fire | -| `+0x39C` | jump | -| `+0x3E0` | zoom | - -Then it overlays emulator-side held / pressed / released bits into `player+0x464`. - -Zoom mask selection inside the overlay: +Standard presses `m_presetBindings.Zoom` while the host Zoom control is held: ```cpp -if (m_enableNativeZoomToggle) - zoomMask = 0; -else if (m_enableNewZoomInputMethod) - zoomMask = ReadBindingLow(player + 0x3E0); -else - zoomMask = 1 << INPUT_R; +m_inputMaskFast &= ~m_presetBindings.Zoom; ``` -So New2 removes zoom from the overlay path entirely. Movement, fire, and jump can still be overlaid, but zoom is handled only by the native setter toggle path. - -## New Method 2 +The game receives ordinary DS input and owns all capability checks, scope +state, HUD, sound, and release behaviour. Earlier implementations used a fixed +`INPUT_R` unless an experimental method was selected; that description is no +longer current. Touch R, Touch L, Dual R, and Dual L now use their own preset +binding without opting into another zoom method. -New2 is native toggle mode. It does not hold an input bit and does not use `player+0x3E0`. - -The external zoom button is treated as a pressed-edge toggle: - -```text -down now = true, down prev = false -> one toggle request -held = true, down prev = true -> no repeated toggles -released -> no action -``` +## New Method 2: native weapon-action toggle -On a pressed edge: +New2 treats the host key as a pressed-edge toggle. Holding the key does not +repeat the request: ```text -1. read local player from ROM-specific global pointer -2. read current zoom state from player+0x850 bit0 -3. desired = !current -4. if desired=true, require current weapon zoom-capable: - player+0x858 -> weapon - weapon+0x08 & 0x00000800 -5. queue native setter call: - SetPlayerScopeZoom(player, desired ? 1 : 0) +up -> down queue one desired-state request +held no additional toggle +down -> up update the latch only ``` -Zoom-off is allowed even if the current weapon is no longer zoom-capable. This is a failsafe for stale scope state after weapon changes or odd game states. +The host side reads the local player, `player+0x850` bit 0 for current scope +state, and the current weapon pointer at `player+0x858`. Enabling scope requires +`weapon+0x08` bit `0x800`; disabling scope remains allowed even after the player +switches to a non-zoom-capable weapon. An idle failsafe queues scope-off if the +scope is active and the current weapon is no longer capable. -New2 also has an idle failsafe: +The request is consumed at the ROM's weapon action update and calls the game's +`SetPlayerScopeZoom(player, enabled)` through the trampoline at `0x02003F00`. +Scratch state begins at `0x02003F40`. The native setter, rather than the host, +owns the state flags, sound, Imperialist crosshair action, and FOV transition. -```text -if current zoom is enabled -and current weapon is not zoom-capable -then queue SetPlayerScopeZoom(player, 0) -``` - -It never writes `player+0x850` or `player+0x648` directly. Scope flags, SFX, HUD, crosshair, and FOV progression are left to the game's native setter/update code. - -## New2 ARM9 Hook - -New2 registers a ROM-specific `WeaponActionUpdate` hook address through `MelonPrimeArm9Hook`. - -The dispatcher order checks `Dispatch_NativeZoomToggle` before `Dispatch_ImmediateInputEdgeOverlay`. This matters because several versions share the same action-consumer address. If a native zoom pending call exists, the zoom redirect happens first. After the trampoline returns to the original hook address with pending cleared, the original instruction path can continue and side-effect hooks can run normally. - -### Address Table - -| Version | LocalPlayerPtrGlobal | SetPlayerScopeZoom | WeaponActionUpdate | +| ROM | local-player pointer global | setter | weapon-action hook | | --- | ---: | ---: | ---: | -| JP1_0 | `020BE790` | `02015C98` | `02024174` | -| JP1_1 | `020BE750` | `02015C98` | `02024174` | -| US1_0 | `020BCA70` | `02015CB8` | `02024198` | -| US1_1 | `020BD2D0` | `02015CBC` | `02024198` | -| EU1_0 | `020BD2F0` | `02015CB0` | `02024190` | -| EU1_1 | `020BD370` | `02015CBC` | `02024198` | -| KR1_0 | `020B6240` | `0201CEBC` | `0200D07C` | - -### Trampoline - -New2 uses an ARM9 trampoline in the shared ROM code cave. The cave is nearly full and its -reservation table is canonical in -[patch-system.md](../../architecture/gameplay/patch-system.md) — read it before moving or resizing -this one. - -```text -trampoline = 02003F00 -scratch = 02003F40 -``` - -Scratch layout: - -| Offset | Value | -| ---: | --- | -| `+0x00` | player pointer | -| `+0x04` | enabled, low byte used | -| `+0x08` | SetPlayerScopeZoom address | -| `+0x0C` | return address, currently WeaponActionUpdate hook address | - -Trampoline flow: - -```text -save r0-r12, lr -r0 = scratch.player -r1 = scratch.enabled -r3 = scratch.setter -blx r3 -restore r0-r12, lr -branch back to scratch.returnAddr -``` - -The pending request is cleared before redirecting to the trampoline. When the trampoline branches back to the hook address, the second dispatcher pass sees no pending request and falls through to the game's original instruction stream. - -## Method Comparison - -| Method | Uses DS input bit | Uses game binding | Uses ARM9 native setter | Toggle edge only | Main risk | -| --- | --- | --- | --- | --- | --- | -| Legacy | yes, fixed `R` | no | no | no | wrong preset on Dual-style controls | -| New2 | no | no | yes | yes | trampoline/code-cave correctness | -| New3 | no | no | yes | yes | trampoline/code-cave correctness; refuses in Alt-Form, mid-transform, or with a non-zoom weapon | - -## Files +| JP1.0 | `0x020BE790` | `0x02015C98` | `0x02024174` | +| JP1.1 | `0x020BE750` | `0x02015C98` | `0x02024174` | +| US1.0 | `0x020BCA70` | `0x02015CB8` | `0x02024198` | +| US1.1 | `0x020BD2D0` | `0x02015CBC` | `0x02024198` | +| EU1.0 | `0x020BD2F0` | `0x02015CB0` | `0x02024190` | +| EU1.1 | `0x020BD370` | `0x02015CBC` | `0x02024198` | +| KR1.0 | `0x020B6240` | `0x0201CEBC` | `0x0200D07C` | + +The pending request is cleared before the PC redirects. After the setter +returns, the trampoline branches back to the hook address; the second +dispatcher pass sees no pending request and resumes the original instruction +stream. + +## New Method 3: DirectInvocation + +New3 uses the same pressed-edge and native-setter semantics but consumes its +mailbox from the game's player input update after first running the original +`ProcessTouchInput` callee. It shares the DirectInvocation trampoline region +`0x02003F50..0x02003FBB` with native weapon/transform requests and does not own +a separate guest data block. + +At the guest safe point it rejects scope-on when any of these are true: + +- `player+0x4C4` bit 9: Alt-Form; +- `player+0x4C4` bit 11: Morphing; +- `player+0x4C4` bit 12: Unmorphing; +- the equipped weapon at `player+0x858` lacks `WeaponData+0x08` bit `0x800`. + +The state checked is therefore the state acted on by the setter, not a +potentially stale host-side snapshot. Scope-off cleanup intentionally bypasses +the weapon capability check. + +| ROM | player-input hook | original `ProcessTouchInput` | setter | +| --- | ---: | ---: | ---: | +| JP1.0 / JP1.1 | `0x020263DC` | `0x02026BFC` | `0x02015C98` | +| US1.0 | `0x02026400` | `0x02026C20` | `0x02015CB8` | +| US1.1 | `0x02026400` | `0x02026C20` | `0x02015CBC` | +| EU1.0 | `0x020263F8` | `0x02026C18` | `0x02015CB0` | +| EU1.1 | `0x02026400` | `0x02026C20` | `0x02015CBC` | +| KR1.0 | `0x0200CF1C` | `0x0200C29C` | `0x0201CEBC` | -| File | Role | -| --- | --- | -| `MelonPrimeDef.h` | `ZoomMethod` config key and `0/1/2` constants | -| `Config.cpp` | default value and range | -| `InputConfig/MelonPrimeInputConfig.cpp` | INPUT METHOD UI checkboxes and mutual exclusion | -| `InputConfig/MelonPrimeInputConfigConfig.cpp` | save `ZoomMethod` | -| `MelonPrime.cpp` | load runtime flags | -| `MelonPrimeGameInput.cpp` | normal zoom input routing and include point for zoom hook | -| `MelonPrimePatchImmediateInputEdgeOverlay.inc` | overlay behavior for Legacy/New, zoom disabled for New2 | -| `MelonPrimePatchNativeZoomToggleHook.inc` | New2 native toggle implementation | -| `MelonPrimeArm9Hook.cpp` | ARM9 dispatch mask and address registration | +## Local-player and spawn safety -## Quick Tests +New2 and New3 are match-scoped and refuse a native call while local-player HP +is zero. The gate exists at both producer and dispatcher because the player can +leave play between queue and consumption. An unresolved cached player pointer +counts as alive so cache reconstruction alone does not disable input. -Legacy: +HP becomes nonzero before respawn has finished rebuilding camera, model, gun, +animation, and HUD state. Two additional barriers cover that gap: -```text -1. uncheck both Zoom method boxes -2. hold zoom button -3. verify the preset's own zoom button behaves like the old build -``` +1. While `player+0xE1 != 0` (spawn invulnerability countdown), Zoom uses the + Standard binding path and clears native pending state. The edge latch stays + synchronised so a held key cannot toggle when the countdown ends. +2. The dispatcher drops an already-queued request on the first input hook after + spawn, identified by + `player+0xE1 == (uint8_t)([player+0x404]+0xE2) - 1`. -New2: +Requests are dropped or handled by Standard, never deferred for replay after +respawn. See [Input method selectors](../melonprime-settings/input-methods.md) +for the lifecycle shared with weapon and transform. -```text -1. check Use New Method 2 for Zoom -2. equip Imperialist -3. press zoom once: scope state should enable -4. hold zoom: state should not repeatedly flip -5. press zoom again: scope state should disable -6. switch to a non-zoom weapon while zoomed: failsafe should queue zoom off -``` +## Immediate Input Edge Overlay interaction -New3: +The developer-only Immediate Input Edge Overlay writes host action state into +the guest input structure at the action consumer. Its current zoom mask is: -```text -1. check Use New Method 3 for Zoom (New Method 2 clears itself) -2. equip Imperialist, press zoom once: scope enables, game plays its own SFX -3. press again: scope disables -4. switch to a non-zoom weapon while zoomed: scope turns itself off -5. transform to Alt-Form and press zoom: nothing happens -6. press zoom during the transform animation: nothing happens -7. start a match while holding zoom: no toggle fires on the first frame +```cpp +zoomMask = (!genericOverlay || m_enableNativeZoomToggle) + ? 0 + : ReadBindingLow(player + 0x3E0); ``` -Useful memory checks for New2: - -```text -player+0x850 bit0 = native zoom enabled state -player+0x858 = current weapon pointer -weapon+0x08 bit 0x800 = zoom-capable flag -``` +Consequences: + +- Standard uses the preset Zoom binding in both normal and overlay paths. +- New2 suppresses overlay zoom, leaving only the native setter request. +- New3 is not included in the suppression condition. With both options enabled, + the overlay can inject the preset Zoom bit in addition to DirectInvocation. + +The last case is the current implementation, not an intended isolation +guarantee. Test it explicitly for duplicate or conflicting actions when either +feature changes. + +## Method comparison + +| Property | Standard | New2 | New3 | +| --- | --- | --- | --- | +| guest sees ordinary bound input | yes | no | no, except current overlay interaction | +| uses active preset binding | yes | no | no | +| calls native setter | no | yes | yes | +| one action per pressed edge | game input semantics | host latch | host latch | +| Alt-Form behaviour | Morph Ball Boost binding | same outer fallback | same outer fallback | +| spawn window | game input | Standard fallback | Standard fallback | +| build availability | all | developer | developer | + +## Ownership and evidence + +Current melonPrimeDS source is authoritative for selectable values and runtime +behaviour: + +- `src/frontend/qt_sdl/Config.cpp` +- `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp` +- `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfigConfig.cpp` +- `src/frontend/qt_sdl/MelonPrimeRuntimeConfig.cpp` +- `src/frontend/qt_sdl/MelonPrimeGameInput.cpp` +- `src/frontend/qt_sdl/MelonPrimePatchNativeZoomToggleHook.inc` +- `src/frontend/qt_sdl/MelonPrimePatchDirectInvocationHook.inc` +- `src/frontend/qt_sdl/MelonPrimePatchImmediateInputEdgeOverlay.inc` +- `src/frontend/qt_sdl/MelonPrimeArm9Hook.cpp` + +Supporting reverse-engineering context is maintained separately in: + +- `C:\Users\Admin\Documents\git\mphCodex\mnt\data\analysis\mphAnalysis\topics\input\Input-Direct-Injection-Investigation\current\summary\Input-Direct-Injection-Consolidated-AllVersions.md` +- `C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\TouchscreenInput\DirectInvocation\current\Direct-Invocation-Spawn-Freeze-Investigation-JP1_0.md` + +Do not copy those investigations here wholesale. They are evidence and design +history; this page records the current melonPrimeDS contract. + +## Verification matrix + +- Save/reload each value and verify exactly one UI choice remains checked. +- Test all four in-game control presets; Standard must use each preset's Zoom + binding, not fixed `R`. +- In Alt-Form, hold/release Zoom and verify Morph Ball Boost without a delayed + scope toggle after unmorphing. +- For New2/New3, press, hold, release, and press again with Imperialist; each + physical press must toggle once. +- Try Alt-Form, Morphing, Unmorphing, and a non-capable weapon; native scope-on + must be rejected. +- Switch away while scoped; cleanup must call the native setter to turn scope + off. +- Press while dead, on the spawn update, throughout spawn invulnerability, and + one frame after it. Nothing may replay late or freeze the player update. +- Hold Zoom across match start, respawn, focus loss, and method changes. +- Repeat New2 and New3 with Immediate Input Edge Overlay both disabled and + enabled, recording the current New3 dual-path interaction. +- Verify all ROM guards before treating hook installation as behavioural proof. diff --git a/docs/features/melonprime-settings.md b/docs/features/melonprime-settings.md index 59a05e2d7e..8c99d19a1d 100644 --- a/docs/features/melonprime-settings.md +++ b/docs/features/melonprime-settings.md @@ -42,21 +42,21 @@ patch lifecycle. | UI area | Functions covered | Detailed reference | | --- | --- | --- | -| Dialog-level | Menu language, system/default language selection | [menu-language.md](melonprime-settings/menu-language.md) | +| Dialog-level | Menu language, system/default language selection, responsive wrapping and scroll behavior | [menu-language.md](melonprime-settings/menu-language.md), [Settings UI and edit mode](../development/ui/settings-and-edit-mode.md#settings-tab-organization-and-responsive-presentation) | | Section headers | Expand/collapse state for all MelonPrime Settings sections | [section-disclosure.md](melonprime-settings/section-disclosure.md) | | Sensitivity | MPH sensitivity, aim sensitivity, Y scale, aim adjustment, accumulator, smoothing, zoom scale, reset button | [sensitivity.md](melonprime-settings/sensitivity.md) | | Aim follow | MPH-native, Immediate Sync, MoonLike Aim, Instant Aim Follow migration alias, FPS Camera Lock | [aim-follow.md](melonprime-settings/aim-follow.md) | | Morph Ball boost | Swipe boost enable/disable, raw-threshold mode, required movement | [morph-ball-boost.md](melonprime-settings/morph-ball-boost.md) plus [existing feature reference](morph-ball-boost.md) | -| Input compatibility | Joy2Key, Stylus Mode, top-screen touch, touch-screen aim only, in-game cursor hiding, top-screen confinement, idle center hold | [input-compatibility.md](melonprime-settings/input-compatibility.md), [stylus-cursor-policy.md](melonprime-settings/stylus-cursor-policy.md) | +| Input compatibility | Joy2Key, Stylus Mode, top-screen touch, touch-screen aim only, temporary Standard-transform exception, in-game cursor hiding, top-screen confinement, idle center hold | [input-compatibility.md](melonprime-settings/input-compatibility.md), [touch-screen-aim-only.md](melonprime-settings/touch-screen-aim-only.md), [stylus-cursor-policy.md](melonprime-settings/stylus-cursor-policy.md) | | Bug Fixes | Wi-Fi active friend/rival bitset, Wi-Fi reconnect Error 52200, Shadow Freeze, Noxus Blade persistence | [wifi-bitset.md](melonprime-settings/wifi-bitset.md), [existing reconnect reference](gameplay/wifi-reconnect-52200.md), [shadow-freeze.md](melonprime-settings/shadow-freeze.md), [noxus-blade-persistence.md](melonprime-settings/noxus-blade-persistence.md) | -| Game Feature Improvements | DS firmware language, online HEADSHOT, online enemy HP indicator, base/extra stage matrix | [firmware-language.md](melonprime-settings/firmware-language.md), [online-headshot.md](melonprime-settings/online-headshot.md), [online-enemy-hp.md](melonprime-settings/online-enemy-hp.md), [stage-matrix.md](melonprime-settings/stage-matrix.md) | +| Game Feature Improvements | DS firmware language, online HEADSHOT, online enemy target information, base/extra stage matrix | [firmware-language.md](melonprime-settings/firmware-language.md), [online-headshot.md](melonprime-settings/online-headshot.md), [online-enemy-hp.md](melonprime-settings/online-enemy-hp.md), [stage-matrix.md](melonprime-settings/stage-matrix.md) | | Disable Features | Disable Double Damage Multiplier, Damage Notify Purple | [damage-multiplier.md](melonprime-settings/damage-multiplier.md), [damage-notify-purple.md](melonprime-settings/damage-notify-purple.md) | | Power-Up Pickup Effects | Parent switch plus Double Damage, Cloak, and Deathalt child switches | [powerup-pickup.md](melonprime-settings/powerup-pickup.md) | | Gameplay Toggles | SnapTap, unlock save data, headphone audio, DS name | [snaptap.md](melonprime-settings/snaptap.md), [save-data-unlock.md](melonprime-settings/save-data-unlock.md), [headphone-audio.md](melonprime-settings/headphone-audio.md), [ds-name.md](melonprime-settings/ds-name.md) | | Video Quality | Low, High, High2, and conditional Metal presets | [video-presets.md](melonprime-settings/video-presets.md) | | Volume | SFX and music apply switches and levels | [volume.md](melonprime-settings/volume.md) | | License Apply | Hunter and rank-color selection/apply switches | [hunter-license.md](melonprime-settings/hunter-license.md) | -| Input Method | Native/legacy weapon switch, biped fire, transform, and zoom methods | [input-methods.md](melonprime-settings/input-methods.md) | +| Input Method | Standard/New/New 2 weapon and transform selectors, developer Biped fire, and Standard/developer-native zoom selectors | [input-methods.md](melonprime-settings/input-methods.md) | | Screen Sync | Off, `glFinish`, and Windows `DwmFlush` modes | [screen-sync.md](melonprime-settings/screen-sync.md) | | Cursor Clip | Bottom-screen cursor clipping outside gameplay and stylus cursor presentation/confinement | [cursor-layout.md](melonprime-settings/cursor-layout.md), [stylus-cursor-policy.md](melonprime-settings/stylus-cursor-policy.md) | | In-Game Apply | In-game top-screen-only layout policy | [cursor-layout.md](melonprime-settings/cursor-layout.md) | diff --git a/docs/features/melonprime-settings/README.md b/docs/features/melonprime-settings/README.md index 44c56b3cdb..0d8ebd4fdb 100644 --- a/docs/features/melonprime-settings/README.md +++ b/docs/features/melonprime-settings/README.md @@ -32,7 +32,7 @@ Each document uses the same evidence boundary: | Noxus Blade persistence | [noxus-blade-persistence.md](noxus-blade-persistence.md) | | DS firmware language | [firmware-language.md](firmware-language.md) | | Online HEADSHOT notification | [online-headshot.md](online-headshot.md) | -| Online enemy HP meter | [online-enemy-hp.md](online-enemy-hp.md) | +| Online enemy target information | [online-enemy-hp.md](online-enemy-hp.md) | | Stage/mode matrix expansion | [stage-matrix.md](stage-matrix.md) | | Disable Double Damage Multiplier | [damage-multiplier.md](damage-multiplier.md) | | Damage Notify Purple | [damage-notify-purple.md](damage-notify-purple.md) | @@ -44,7 +44,7 @@ Each document uses the same evidence boundary: | Video quality presets | [video-presets.md](video-presets.md) | | SFX and music volume | [volume.md](volume.md) | | Hunter License hunter/color | [hunter-license.md](hunter-license.md) | -| Joy2Key, Stylus, and touch behavior | [input-compatibility.md](input-compatibility.md), [stylus-cursor-policy.md](stylus-cursor-policy.md) | +| Joy2Key, Stylus, and touch behavior | [input-compatibility.md](input-compatibility.md), [touch-screen-aim-only.md](touch-screen-aim-only.md), [stylus-cursor-policy.md](stylus-cursor-policy.md) | | Weapon, transform, fire, and zoom methods | [input-methods.md](input-methods.md) | | Screen synchronization | [screen-sync.md](screen-sync.md) | | Cursor clip and in-game layout | [cursor-layout.md](cursor-layout.md), [stylus-cursor-policy.md](stylus-cursor-policy.md) | diff --git a/docs/features/melonprime-settings/input-compatibility.md b/docs/features/melonprime-settings/input-compatibility.md index 4678f32399..3e3c376596 100644 --- a/docs/features/melonprime-settings/input-compatibility.md +++ b/docs/features/melonprime-settings/input-compatibility.md @@ -11,10 +11,10 @@ native MelonPrime aim path: | Stylus mode | Metroid.Enable.stylusMode | false | | Top-screen touch | Metroid.Enable.topScreenTouch | false | | Touch-screen aim only | Metroid.Enable.touchScreenAimOnly | false | +| Temporarily restore HUD hit-testing for a Standard transform | Metroid.Enable.touchScreenAimOnlySuspendForTransform | true | | Hide stylus cursor in game | Metroid.Enable.stylusHideCursorInGame | false | | Confine stylus cursor to top screen | Metroid.Enable.stylusConfineCursorToTopScreen | false | | Hold stylus cursor at center while not clicking | Metroid.Enable.stylusHoldCursorAtCenterWhenNotClicking | false | -| Direct alt-form transform | Metroid.Input.Enable.DirectAltFormTransform | false | The controls are not interchangeable: @@ -22,11 +22,13 @@ The controls are not interchangeable: - Stylus mode selects touch/stylus aim semantics and disables incompatible native aim controls. - Top-screen touch enables a UI routing path. -- Touch-screen aim only is a guarded guest hit-test patch. +- Touch-screen aim only is a guarded guest hit-test patch and is applied only + while Stylus Mode is also enabled. +- Its indented Standard-transform option briefly restores the Transform-button + hit-test around the legacy simulated tap, then reapplies the owning patch. - Hide, top-screen confinement, and hold-at-center are host cursor policies; their complete contract is in [stylus-cursor-policy.md](stylus-cursor-policy.md). -- Direct alt-form transform selects a native guest transform hook. ## Joy2Key compatibility support @@ -59,10 +61,11 @@ The direct stylus transform path remains active. Thus a disabled spin control does not mean stylus aim is disabled; it means the normal mouse/controller scaling path is not being used. -Stylus mode and Touch-screen aim only are also different. Stylus mode is a -host input mode. Touch-screen aim only patches a guest control hit-test so -touch input is treated as the aim source. They may be used together, but one -must not be used as evidence that the other is working. +Stylus mode and Touch-screen aim only remain different layers, but the current +patch deliberately requires both configuration values. Stylus Mode is the host +input mode; Touch-screen aim only patches three guest HUD hit-tests. A checked +child value retained while Stylus Mode is off is inactive rather than an +independent patch request. ## Top-screen touch and cursor presentation @@ -86,25 +89,10 @@ combination matrix, and platform boundary. ## Touch-screen aim only patch -The patch writes mov r0,#0, value 0xE3A00000 at three guarded call/hit-test -sites per ROM. The exact original call words are version-specific and are -stored in MelonPrimePatchTouchScreenAimOnly.cpp. - -| ROM | Site 1 | Site 2 | Site 3 | -| --- | ---: | ---: | ---: | -| JP1.0 / JP1.1 | 0x02026C70 | 0x02026E40 | 0x02026F70 | -| US1.0 | 0x02026C94 | 0x02026E64 | 0x02026F94 | -| US1.1 / EU1.1 | 0x02026C94 | 0x02026E64 | 0x02026F94 | -| EU1.0 | 0x02026C8C | 0x02026E5C | 0x02026F8C | -| KR1.0 | 0x0200C308 | 0x0200C4C8 | 0x0200C5E8 | - -Apply requires every site to match its ROM-specific original or already -applied value. Restore requires the replacement value and writes back only -the owned site words. The registry applies the patch on battle runtime/config -reload and restores it on leave/stop. - -The double-tap gesture and touch boost are separate behavior and are not -covered by these three addresses. +The complete patch words, parent/child state matrix, Standard-transform +bracketing sequence, lifecycle, and mphCodex research pointers are maintained +in [Touch-screen aim only](touch-screen-aim-only.md). This page keeps only the +compatibility relationship so the ROM table has one owner. ## Compatibility matrix @@ -112,8 +100,9 @@ covered by these three addresses. | --- | --- | | Normal input, stylus off | Ordinary sensitivity/native settings apply | | Stylus on | Stylus transform owns aim; many normal aim controls are bypassed | -| Touch-only on, stylus off | Guest touch hit-test patch plus normal host routing | -| Stylus on + touch-only on | Two distinct layers; test both independently | +| Touch-only checked, stylus off | Saved parent value is retained, but the guest patch is restored/inactive | +| Stylus on + touch-only on | Stylus host mode plus the three-site guest hit-test patch | +| Standard transform + temporary exception on | Patch is restored for the simulated Transform tap, then reapplied | | Hide cursor on | Presentation change only | | Top-screen confinement on | Windows host clip in the active stylus state; not aim capture | | Hold cursor at center on | Host warp while no stylus click is held; active drags are not continuously recentered | @@ -123,7 +112,8 @@ covered by these three addresses. - Record all eight controls before comparing input behavior. - Test stylus mode with the controls that the UI disables. -- Test touch-only on every ROM in the patch table. +- Test touch-only and its Standard-transform exception on every ROM in the + dedicated patch table. - Verify guard failure and leave/stop restoration. - Test top-screen touch with each screen layout used by the deployment. - Confirm cursor hiding does not change aim deltas. diff --git a/docs/features/melonprime-settings/input-methods.md b/docs/features/melonprime-settings/input-methods.md index 7ac9f27a51..8c72a67a67 100644 --- a/docs/features/melonprime-settings/input-methods.md +++ b/docs/features/melonprime-settings/input-methods.md @@ -5,13 +5,13 @@ The Input Method section selects how weapon switching, biped firing, transform, and zoom requests are generated: -| Control | Key | Default | Values | -| --- | --- | --- | --- | -| Weapon switch method | Metroid.Input.WeaponSwitchMethod | 0 | 0 legacy, 1 native, 2 native 2 | -| Biped fire method | Metroid.Input.BipedFireMethod | 0 | 0 legacy, 1 native | -| Transform method | Metroid.Input.AltFormTransformMethod | 0 | 0 legacy, 1 native gate, 2 native 2 | -| Transform method (legacy key) | Metroid.Input.Enable.DirectAltFormTransform | false | mirrors value 1 of the key above | -| Zoom method | Metroid.Input.ZoomMethod | 0 | 0 legacy, 1 retired alias, 2 native, 3 native 3 | +| Control | Key | Default | Values | Availability | +| --- | --- | --- | --- | --- | +| Weapon switch method | Metroid.Input.WeaponSwitchMethod | 0 | 0 Standard, 1 New, 2 New 2 | all builds | +| Biped fire method | Metroid.Input.BipedFireMethod | 0 | 0 Standard, 1 New | New is developer-only | +| Transform method | Metroid.Input.AltFormTransformMethod | 0 | 0 Standard, 1 New, 2 New 2 | all builds | +| Transform method (migration key) | Metroid.Input.Enable.DirectAltFormTransform | false | mirrors value 1 of the key above | compatibility only; no separate control | +| Zoom method | Metroid.Input.ZoomMethod | 0 | 0 Standard, 1 retired alias, 2 New 2, 3 New 3 | only Standard in release builds | Each domain stores one integer, so its methods are mutually exclusive by construction, and value 0 -- the behaviour you get when no new method is @@ -53,14 +53,16 @@ Action Consumer. Its ROM-specific PC is: | EU1.0 / EU1.1 | 0x02024190 / 0x02024198 | | KR1.0 | 0x0200F6DC | -The hook dispatcher also shares the post-poll player input update for -immediate edges. This shared boundary is why enabling multiple native -methods must be tested for duplicate dispatch. The source owns the -single-dispatch rule. +The hook dispatcher also shares the post-poll player input update for immediate +edges. Different domains can select native methods at the same time, so a +single input update may have more than one kind of request pending. The +dispatcher order and per-request clearing in source own the single-dispatch +rule; the UI's exactly-one grouping applies within a domain, not across +domains. ## Weapon switch -Value 0 retains the legacy touch/menu-driven path. Value 1 hooks the native +Value 0 retains the Standard touch/menu-driven path. Value 1 hooks the native TryEquipWeapon path. The native path uses a trampoline at 0x02003EA0 and a scratch area at 0x02003EE0. @@ -84,7 +86,7 @@ after leave/stop. ## Biped fire -Value 0 uses the legacy input path. Value 1 consumes a native fire edge at the +Value 0 uses the Standard input path. Value 1 consumes a native fire edge at the shared action boundary. The helper sets the fire result true for the native edge; guest cooldown, ammo, projectile creation, HUD, and sound remain owned by the game. @@ -94,9 +96,11 @@ empty ammo, weapon changes, and pause/menu transitions. ## Transform -When Direct Alt-form Transform is false, transform continues through the -legacy simulated input path. When true, the native transform gate hooks the -guest TransformRequest condition/call pair. +Value 0 uses the Standard simulated-input path. Value 1 hooks the guest +TransformRequest condition/call pair, and value 2 uses DirectInvocation. The +legacy boolean key is only migration/compatibility storage: false maps to value +0 and true maps to value 1 when the integer key is absent. It cannot select +value 2. | ROM | Compare site | Transform call site | | --- | ---: | ---: | @@ -159,13 +163,15 @@ three conventions: `ApplyOnBattleRuntimeEnter`, exactly as the Method-1 weapon trampoline is. The first dispatch must not have to write 27 words of guest code, and invalidate the JIT blocks covering them, from inside the hook callback. -- **The weapon path keeps the Method-1 spawn-window guard.** While spawn - invincibility is still counting down the native equip is unsafe, so that one - request goes to the legacy route instead. +- **Every native path uses the full spawn-window guard.** While spawn + invincibility is still counting down, weapon, transform, and zoom use their + Standard path for that input instead of making a native call. This is in + addition to the first-post-spawn dispatcher barrier described below. -Method 2 and the Method-1 weapon hook share one hook PC. When both are enabled -and both have a pending request on the same frame, Method 1 dispatches and the -Method-2 request is retried on the next frame within its TTL. +DirectInvocation and the New weapon path share one hook PC, but they cannot be +selected simultaneously for the weapon domain because the selector stores one +integer. A request from another domain can still share the same dispatcher +frame, which is why combined-domain tests remain necessary. ### Zoom (New Method 3) @@ -269,7 +275,7 @@ There are two layers, because they cover different cases. **Whole window, producer side.** While the local player is inside the spawn invulnerability countdown (`player+0xE1 != 0`), every native method hands the request to the Standard path instead of making a native call: weapon goes -through the legacy touch route, and transform and zoom fall through to their +through the Standard touch route, and transform and zoom fall through to their Standard branches. The native request is cleared rather than left pending, and zoom keeps its shared pressed-edge latch in sync so leaving the window mid-hold cannot fire a stale toggle. @@ -297,7 +303,8 @@ During the spawn window the input still does something -- it runs the Standard method -- rather than being swallowed. The trade is that the selected method is briefly not the one in use. -Evidence: mphCodex `Direct-Invocation-Spawn-Freeze-Investigation-JP1_0.md`. +Evidence: +`C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\TouchscreenInput\DirectInvocation\current\Direct-Invocation-Spawn-Freeze-Investigation-JP1_0.md`. ## Lifecycle and interactions @@ -309,7 +316,11 @@ behavioral path as well. Stylus mode can intentionally bypass native aim-related controls. Joy2Key and SnapTap can change the host edge sequence before the native hook sees it. Immediate Input Edge Overlay is a developer diagnostic that shares a post-poll -boundary and must not create duplicate fire/zoom/transform actions. +boundary and must not create duplicate fire/zoom/transform actions. Its current +zoom suppression checks New Method 2 only. New Method 3 therefore still gets +the preset-bound zoom bit overlaid while also using DirectInvocation; treat +that combination as a known interaction requiring an explicit duplicate-action +test, not as evidence that the two paths are isolated. ## Verification checklist diff --git a/docs/features/melonprime-settings/menu-language.md b/docs/features/melonprime-settings/menu-language.md index 6d0d461ffd..75b40f2e15 100644 --- a/docs/features/melonprime-settings/menu-language.md +++ b/docs/features/melonprime-settings/menu-language.md @@ -22,7 +22,8 @@ systems. The persisted value is a language ID. The value -1 means system/default language. The combo box is populated dynamically from AllSelectableMenuLanguages rather than from a hard-coded list duplicated in -the .ui file. +the .ui file. It is always visible on every host locale; an older Japanese-OS +only English/Japanese selector is no longer the current design. The UI layer: @@ -33,6 +34,12 @@ The UI layer: 4. writes the selected ID when the setting is changed; and 5. reloads the visible menu strings through the normal localization path. +The selector is presented in a horizontal language row with a 260 px minimum +and 420 px maximum width. Long native language names therefore remain readable +without forcing a horizontal scrollbar. A language change also reruns Settings +presentation wrapping so translated checkbox captions reflow to the current +viewport width. + The settings tab therefore must not assume that every locale has a guest-side equivalent. A newly registered UI locale may be available even when the ROM firmware language patch has no corresponding byte value. @@ -75,8 +82,8 @@ implementation rather than to the ROM patch table. Current implementation: -- Source: MelonPrimeDef.h, MelonPrimeInputConfig.cpp, and the localization - registration used by the settings UI. +- Source: MelonPrimeDef.h, MelonPrimeInputConfig.cpp, and the registry/catalog + under `src/frontend/qt_sdl/MelonPrimeLocalization/`. - Detailed UI ownership: docs/development/ui/settings-and-edit-mode.md. Related research in the separate mphCodex repository is useful for the guest diff --git a/docs/features/melonprime-settings/morph-ball-boost.md b/docs/features/melonprime-settings/morph-ball-boost.md index c3e2c5dfcc..139a724d59 100644 --- a/docs/features/melonprime-settings/morph-ball-boost.md +++ b/docs/features/melonprime-settings/morph-ball-boost.md @@ -2,7 +2,7 @@ ## Setting contract -These controls are dynamically inserted into the Sensitivity section. They +These controls are dynamically grouped under Input Settings. They configure the MelonPrime mouse-swipe assist; they do not replace the existing right-click boost or the Shift hold-to-boost action described in the broader [Morph Ball Boost feature reference](../morph-ball-boost.md). @@ -102,7 +102,9 @@ current UI controls. frame or consumed delta. - Test Samus versus non-Samus, Morph Ball versus biped, stylus mode, and Joy2Key/SnapTap combinations. -- Test config reload, reset-sensitivity, leave, stop, and rejoin behavior. +- Test config reload, leave, stop, and rejoin behavior. The Sensitivity reset + button does not reset these controls because they now belong to Input + Settings. ## Evidence and related material diff --git a/docs/features/melonprime-settings/online-enemy-hp.md b/docs/features/melonprime-settings/online-enemy-hp.md index aa11381252..9debbc9c7b 100644 --- a/docs/features/melonprime-settings/online-enemy-hp.md +++ b/docs/features/melonprime-settings/online-enemy-hp.md @@ -1,4 +1,4 @@ -# Online enemy HP meter +# Online enemy target information ## Setting contract @@ -6,14 +6,18 @@ | --- | --- | | Key | Metroid.GameFeature.ShowEnemyHpMeterOnline | | Default | false | -| UI | Show Enemy HP Meter Online | -| Source | MelonPrimePatchShowEnemyHpMeterOnline.cpp | +| UI | Show Enemy Target Information Online | +| Source | MelonPrimePatchShowEnemyHpMeterOnline.cpp (legacy internal name) | | Patch kind | Three guarded words, or two on KR | -The option enables the guest's enemy-HP display path during online play. It is -only a rough hit indicator: online enemy HP is not authoritative and is -generally not updated as a reliable continuously synchronized health value. -The UI should not describe this as a real health bar. +The option removes the guest's online early-return so target slot, timer, and +target-information state can be produced during online play. The persisted key +and patch filename retain the older “HpMeter” name for compatibility, but the +visible label deliberately describes the broader data path. + +Any HP-like value is only a rough hit indicator: online enemy HP is not +authoritative and is generally not updated as a reliable continuously +synchronized health value. The UI must not promise a real health bar. ## ROM patch table @@ -38,7 +42,7 @@ on match leave and emulator stop and can be reapplied after ConfigReload. ## Behavior boundary -- It is a local rendering/display change. +- It is a local target-information/rendering change. - It does not make remote HP synchronized. - It does not change weapon damage, hit registration, or server authority. - A visible bar that remains stale is an expected limitation of the guest diff --git a/docs/features/melonprime-settings/section-disclosure.md b/docs/features/melonprime-settings/section-disclosure.md index cae38ca4b0..6896053c19 100644 --- a/docs/features/melonprime-settings/section-disclosure.md +++ b/docs/features/melonprime-settings/section-disclosure.md @@ -15,25 +15,28 @@ checkbox or combo value. | Section | Key | Default | | --- | --- | --- | | Sensitivity | Metroid.UI.SectionSensitivity | expanded | -| Bug Fixes | Metroid.UI.SectionBugFix | expanded | +| Input Settings | Metroid.UI.SectionInputSettings | collapsed | +| Input Method | Metroid.UI.SectionInputMethod | collapsed | +| Gameplay Toggles | Metroid.UI.SectionGameplay | expanded | | Game Feature Improvements | Metroid.UI.SectionGameFeature | expanded | +| Power-Up Pickup Effects (nested) | Metroid.UI.SectionPowerUpPickupEffects | expanded | | Disable Features | Metroid.UI.SectionDisableFeatures | expanded | -| Power-Up Pickup Effects | Metroid.UI.SectionPowerUpPickupEffects | expanded | -| Gameplay Toggles | Metroid.UI.SectionGameplay | expanded | +| Low HP Warning | Metroid.UI.SectionLowHpWarning | collapsed | +| In-Game Apply | Metroid.UI.SectionInGameApply | collapsed | +| In-Game Aspect Ratio | Metroid.UI.SectionInGameAspectRatio | collapsed | +| Screen Sync | Metroid.UI.SectionScreenSync | collapsed | +| Cursor Clip Settings | Metroid.UI.SectionCursorClipSettings | collapsed | | Video Quality | Metroid.UI.SectionVideo | expanded | | Volume | Metroid.UI.SectionVolume | expanded | | License Apply | Metroid.UI.SectionLicense | expanded | -| Input Settings | Metroid.UI.SectionInputSettings | collapsed | -| Input Method | Metroid.UI.SectionInputMethod | collapsed | -| Screen Sync | Metroid.UI.SectionScreenSync | collapsed | -| Cursor Clip Settings | Metroid.UI.SectionCursorClipSettings | collapsed | -| In-Game Apply | Metroid.UI.SectionInGameApply | collapsed | -| In-Game Aspect Ratio | Metroid.UI.SectionInGameAspectRatio | collapsed | -| Low HP Warning | Metroid.UI.SectionLowHpWarning | collapsed | +| Bug Fixes | Metroid.UI.SectionBugFix | expanded | | Developer Only | Metroid.UI.SectionDeveloperOnly | collapsed | The default state is the source-defined initial UI state. A user's saved expanded/collapsed choice should not be interpreted as a feature default. +The table follows current visible task order; Power-Up Pickup Effects is a +nested disclosure inside Game Feature Improvements rather than a top-level +Settings page section. ## Save behavior diff --git a/docs/features/melonprime-settings/touch-screen-aim-only.md b/docs/features/melonprime-settings/touch-screen-aim-only.md new file mode 100644 index 0000000000..ca35f5fdd0 --- /dev/null +++ b/docs/features/melonprime-settings/touch-screen-aim-only.md @@ -0,0 +1,164 @@ +# Touch-screen aim-only and Standard-transform exception + +## Setting contract + +These two settings are a parent/child pair in **Input Settings**: + +| UI control | Configuration key | Default | +| --- | --- | ---: | +| Use the Whole Touch Screen for Aiming | Metroid.Enable.touchScreenAimOnly | false | +| Turn It Off Briefly While a Standard-Method Transform Fires | Metroid.Enable.touchScreenAimOnlySuspendForTransform | true | + +The child default is intentionally true, but it has no independent effect. The +dialog enables it only while **Stylus Mode** and the parent option are checked. +The patch module applies the parent only when both of these are true: + +```text +Metroid.Enable.stylusMode +AND Metroid.Enable.touchScreenAimOnly +``` + +If Stylus Mode is turned off while the aim-only value remains saved, the patch +is restored. A checked but disabled child widget therefore means “remember this +choice for the next active parent state,” not “the exception is active now.” + +## What the parent patch changes + +The in-match player touch dispatcher tests three bottom-screen HUD areas: + +| Region | IDs | Normal action | +| --- | --- | --- | +| Transform / Unmorph | 4 | Request a form change | +| Weapon quick slots | 0–2 | Select a slot weapon, including the touch zoom shortcut | +| Weapon radial menu | 3 | Open/process the radial menu | + +Each ROM call site is immediately followed by `cmp r0,#0`. Replacing the +hit-test call with `0xE3A00000` (`mov r0,#0`) takes the existing non-hit branch. +The touch can then continue as aim input without setting `NoAimInput` or +dispatching the corresponding HUD action. + +This is deliberately narrower than disabling the shared hit-test function or +clearing `NoAimInput` globally. The shared function is used by other UI/menu +paths, and forcibly clearing the bit would not prevent the HUD action itself. + +The patch does not alter the separate double-tap jump or touch boost gesture +handlers. Those gestures can therefore remain active while the three HUD areas +are neutralized. + +## ROM patch table + +All three applied words are `0xE3A00000`. The table records the guarded +original call word for every site; matching an address alone is not sufficient +authorization to write it. + +| ROM | Transform ID4 | Quick slots ID0–2 | Weapon menu ID3 | +| --- | --- | --- | --- | +| JP1.0 | `0x02026C70 / 0xEB004943` | `0x02026E40 / 0xEB0048CF` | `0x02026F70 / 0xEB004883` | +| JP1.1 | `0x02026C70 / 0xEB004943` | `0x02026E40 / 0xEB0048CF` | `0x02026F70 / 0xEB004883` | +| US1.0 | `0x02026C94 / 0xEB0048F7` | `0x02026E64 / 0xEB004883` | `0x02026F94 / 0xEB004837` | +| US1.1 | `0x02026C94 / 0xEB0048D0` | `0x02026E64 / 0xEB00485C` | `0x02026F94 / 0xEB004810` | +| EU1.0 | `0x02026C8C / 0xEB0048D0` | `0x02026E5C / 0xEB00485C` | `0x02026F8C / 0xEB004810` | +| EU1.1 | `0x02026C94 / 0xEB0048D0` | `0x02026E64 / 0xEB00485C` | `0x02026F94 / 0xEB004810` | +| KR1.0 | `0x0200C308 / 0xEB007E20` | `0x0200C4C8 / 0xEB007DB0` | `0x0200C5E8 / 0xEB007D68` | + +Each cell is `address / original word`. `StaticWordPatch` treats the three +words as one ROM-owned span: apply accepts original or already-applied values, +and an unknown foreign word rejects the operation instead of partially +patching the dispatcher. Restore writes back only owned applied words. + +## Why Standard transform needs an exception + +The Standard transform method does not call the native transform routine +directly. It simulates a tap on touch region ID4 at the preset-mirrored center +of the Transform button. The parent patch makes that exact hit-test return +false, so without an exception Standard transform cannot fire while aim-only +is active. + +The child option brackets only that synthetic tap: + +```text +HandleRareMorph() + -> TouchScreenAimOnly_RestoreOnce() + -> release touch + -> advance two frames + -> press preset-mirrored Transform center + -> advance two frames + -> release touch + -> advance two frames + -> TouchScreenAimOnly_ApplyOnce() +``` + +Reapplication goes through the patch owner and re-reads the current config. It +does not blindly rewrite the three words. This matters if the user changed +Stylus Mode or the parent setting while the sequence was running. + +The exception applies only when execution reaches the Standard touch path. +Transform New Method and New Method 2 do not synthesize this tap and therefore +do not need the hit-test restored. During the spawn-invulnerability window, +native transform methods intentionally fall back to Standard; the same bracket +then protects that temporary Standard path. + +Standard weapon switching is different. Its legacy implementation drives the +weapon-change state and separately brackets the no-double-tap-jump patch; it +does not depend on Transform region ID4. The new child setting must not be +described as a general “temporarily disable aim-only for all input.” + +## Lifecycle and ownership + +`MelonPrimePatchRegistry` applies or restores the parent at `BattleRuntime` and +`ConfigReload`, with restore on leave and stop. The temporary exception is an +in-frame call through `TouchScreenAimOnly_RestoreOnce` and +`TouchScreenAimOnly_ApplyOnce`; it does not add a second patch table or writer. + +Relevant state boundaries: + +| Boundary | Expected result | +| --- | --- | +| Stylus off | Parent patch restored even if the saved aim-only value is true | +| Stylus on, parent off | No guest words owned/applied | +| Stylus on, parent on | All three guarded hit-tests neutralized | +| Standard transform, child on | Restore for the tap sequence, then guarded reapply | +| Standard transform, child off | Transform region remains neutralized; the simulated transform does not fire | +| Native transform method | No temporary restore in the normal native path | +| Leave/stop | Restore all owned words and reset patch state | + +## Verification checklist + +- Confirm the UI child is editable only when Stylus Mode and the parent are + checked, while its saved value survives a temporary disabled state. +- On each ROM, verify all three original words before enabling the parent and + verify all three restore on disable, leave, and stop. +- Test Transform, all three quick slots, and the radial menu with the parent + off/on; the three HUD actions should stop only in the active parent state. +- Confirm free touch aiming still works over the former HUD rectangles. +- Confirm double-tap jump and the touch boost gesture remain separate. +- Test Standard transform with the child on and off, including left-handed + presets whose Transform X coordinate is mirrored. +- Test New Method and New Method 2 transform with the child on; neither should + require a synthetic touch or a temporary patch restore. +- Test a selected native transform method during the spawn-invulnerability + fallback, where Standard temporarily owns the request. +- Force one foreign word at a site in a disposable test environment and verify + the complete apply is rejected rather than partially written. +- Report static guard inspection, build results, and physical gameplay results + separately. + +## Source and research ownership + +Current melonPrimeDS implementation: + +- `src/frontend/qt_sdl/MelonPrimePatchTouchScreenAimOnly.cpp` +- `src/frontend/qt_sdl/MelonPrimeInGame.cpp` +- `src/frontend/qt_sdl/MelonPrimeRuntimeConfig.cpp` +- `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp` +- `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfigConfig.cpp` +- `src/frontend/qt_sdl/Config.cpp` + +The reverse-engineering evidence remains single-sourced in mphCodex: + +- `C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\TouchscreenInput\current\InMatch-Touchscreen-AimOnly-Patch-Map-AllVersions.md` +- `C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\TouchscreenInput\current\InMatch-Touchscreen-Reaction-Areas-AimOnly-Investigation-AllVersions.md` +- `C:\Users\Admin\Documents\git\mphCodex\mphAnalysis\Battle\TouchscreenInput\evidence\functions\02026BFC-player-touch-ui-action-dispatch-JP1_0.md` + +Those files explain the guest hit-test and action flow; this page owns the +current melonPrimeDS configuration, lifecycle, and temporary-restore behavior. diff --git a/docs/features/rendering/README.md b/docs/features/rendering/README.md index a93907fb12..56690667e7 100644 --- a/docs/features/rendering/README.md +++ b/docs/features/rendering/README.md @@ -9,6 +9,7 @@ index, not another renderer implementation document. | --- | --- | | How is the Vulkan renderer structured and presented? | [Vulkan backend](vulkan-backend.md) | | How is the Windows DirectX 12 renderer structured and presented? | [DirectX 12 backend](dx12-backend.md) | +| Which backends force or expose high-resolution vertex coordinates? | [High-resolution vertex coordinates](high-resolution-coordinates.md) | | Why is the macOS OpenGL compute preset restricted? | [macOS compute renderer restriction](macos-compute-renderer-restriction.md) | | What is the backend-neutral ownership contract? | [SRP and performance contract](../../architecture/srp-performance-contract.md) | | How do I build on Windows? | [Windows MinGW build](../../development/build/windows-mingw.md) | diff --git a/docs/features/rendering/dx12-backend.md b/docs/features/rendering/dx12-backend.md index 3a260581d0..10a080e30a 100644 --- a/docs/features/rendering/dx12-backend.md +++ b/docs/features/rendering/dx12-backend.md @@ -1,5 +1,9 @@ # DirectX 12 backend +DirectX 12 forces the shared high-resolution-coordinate option on; the exact +UI, config normalisation, 1x exception, and backend comparison are documented +in [High-resolution vertex coordinates](high-resolution-coordinates.md). + Windows-only 3D renderer, originally added on `develop_dx12` and currently maintained on `develop_hud` at `2a0266f14` (checkout verified 2026-08-28). It is a port of melonDS's **OpenGL compute renderer** (`src/GPU3D_Compute.cpp` + `GPU3D_Compute_shaders.h`) diff --git a/docs/features/rendering/high-resolution-coordinates.md b/docs/features/rendering/high-resolution-coordinates.md new file mode 100644 index 0000000000..39ac87872b --- /dev/null +++ b/docs/features/rendering/high-resolution-coordinates.md @@ -0,0 +1,134 @@ +# High-resolution vertex coordinates + +## Setting contract + +| Item | Value | +| --- | --- | +| Key | `3D.GL.HiresCoordinates` | +| Default | `true` | +| UI | Use high resolution coordinates | +| Scope | global 3D renderer setting | + +The setting chooses which already-calculated DS vertex position a supporting +renderer scales into its rasterizer. At an internal render scale above 1x, +enabled uses `HiresPosition`; disabled uses the ordinary final integer-screen +position. It does not increase the selected render scale by itself and is not a +texture filter, anti-aliasing mode, or Custom HUD coordinate option. + +## Current backend policy + +| Renderer | UI state | Effective policy | +| --- | --- | --- | +| OpenGL Compute | checked and disabled | forced on | +| Vulkan | checked and disabled | forced on | +| DirectX 12 | checked and disabled | forced on | +| Metal raster | enabled control | user value | +| Metal compute | enabled control | user value | +| classic OpenGL | control disabled | this compute/Metal coordinate path is not exposed | +| software | control disabled | not consumed by the software renderer | + +The forced policy is intentional for the three compute-style backends used by +MelonPrimeDS. `VideoSettingsDialog::RendererForcesHiresCoordinates()` returns +true for OpenGL Compute, Vulkan, and DX12. Renderer selection then checks the +box, disables it, and writes `true` to config. The rendering paths also receive +the effective value independently; UI disablement is not the only enforcement +layer. + +Metal remains the comparison/user-choice path. Both Metal raster and Metal +compute store the flag and use it only above 1x. + +## Rasterizer semantics + +OpenGL Compute, Vulkan, and DX12 use the same branch while building polygon +edge positions: + +```cpp +if (HiresCoordinates && ScaleFactor > 1) { + scaledX = (vertex.HiresPosition[0] * ScaleFactor) >> 4; + scaledY = (vertex.HiresPosition[1] * ScaleFactor) >> 4; +} else { + scaledX = vertex.FinalPosition[0] * ScaleFactor; + scaledY = vertex.FinalPosition[1] * ScaleFactor; +} +``` + +`HiresPosition` retains four fractional bits, hence the `>> 4` after scaling. +At 1x the condition is false even when the config is true. That preserves the +native-resolution integer coordinate path and means “forced on” has no visual +effect until internal scale exceeds 1. + +The option is distinct from Improved polygon splitting. Compute, Vulkan, and +DX12 rasterize original DS polygon spans and do not need the classic +OpenGL/Metal center-fan workaround. High-resolution coordinates change vertex +positions; polygon splitting changes how an N-sided polygon becomes GPU +triangles. One setting is not an alias for the other. + +## Live update and resource ownership + +The render-setting handoff is through `EmuThread`'s `GPU::RenderSettings`. +Each supporting backend stores the incoming flag. OpenGL Compute, Vulkan, and +DX12 explicitly update it even when the scale is unchanged, then return without +rebuilding resolution-sized resources. This permits a live policy/value update +without treating it as a renderer restart or scale change. + +The Metal implementations likewise retain their own per-renderer flag. Visual +validation must therefore identify both backend and scale; reading the config +alone does not prove which coordinate branch drew a frame. + +## Dialog normalisation and cancel behaviour + +When Video Settings opens, it snapshots the current renderer and coordinate +value. If the renderer is one of the forced backends, it first normalises both +the snapshot and live config to `true`. As a result: + +- an old config containing `false` becomes `true` merely by opening the dialog + on a forced backend; +- the box cannot be unchecked while that backend remains selected; +- the state-change slot writes `forced || checked` as an additional guard; and +- Cancel restores the normalised `true`, not the stale pre-contract `false`. + +Switching to Metal enables the box and allows a deliberate user value. Switching +back to OpenGL Compute, Vulkan, or DX12 immediately checks and normalises it. + +MelonPrime's renderer preset handlers for the forced backends also set +`3D.GL.HiresCoordinates=true`, so choosing a preset and using the general Video +Settings dialog converge on the same config state. + +## Source ownership + +| Source | Responsibility | +| --- | --- | +| `src/frontend/qt_sdl/Config.cpp` | default and legacy config migration | +| `src/frontend/qt_sdl/VideoSettingsDialog.cpp` | renderer policy, UI enablement, normalisation, apply/cancel | +| `src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp` | MelonPrime renderer preset normalisation | +| `src/frontend/qt_sdl/EmuThread.cpp` | config-to-`GPU::RenderSettings` handoff | +| `src/GPU_OpenGL.cpp` | force `true` when forwarding to OpenGL Compute | +| `src/GPU_Vulkan.cpp` | force `true` when forwarding to Vulkan 3D | +| `src/GPU_DX12.cpp` | force `true` when forwarding to DX12 3D | +| `src/GPU3D_Compute.cpp` | OpenGL Compute storage and edge-position branch | +| `src/GPU3D_Vulkan.cpp` | Vulkan storage and edge-position branch | +| `src/GPU3D_DX12.cpp` | DX12 storage and edge-position branch | +| `src/GPU3D_Metal.mm` | Metal raster user-controlled branch | +| `src/GPU3D_MetalCompute.mm` | Metal compute user-controlled branch | + +This is host renderer behaviour and has no ROM patch address table. mphCodex is +therefore not the authority for this setting. + +## Verification matrix + +- Open Video Settings on OpenGL Compute, Vulkan, and DX12: the box is checked, + disabled, and config is true. +- Seed config false, open each forced backend, cancel, and confirm the value + remains normalised true. +- Switch from a forced backend to Metal and confirm the control becomes + editable; toggle it live at 2x or higher. +- Compare enabled/disabled Metal output at 1x: both must take the ordinary + coordinate branch. +- Compare at 2x and a high scale using geometry that exposes subpixel vertex + movement; record backend, scale, and frame evidence. +- Change only the coordinate value with scale unchanged and confirm the backend + does not rebuild scale-sized resources. +- Confirm classic OpenGL/software do not imply support merely because the + global config key exists. +- Keep source audit, successful backend build, backend initialization, and + visual parity as separate evidence claims. diff --git a/docs/features/rendering/vulkan-backend.md b/docs/features/rendering/vulkan-backend.md index f884436f77..8f7987f611 100644 --- a/docs/features/rendering/vulkan-backend.md +++ b/docs/features/rendering/vulkan-backend.md @@ -141,9 +141,12 @@ would replace the compute renderer's native polygon interpolation rather than improve it. The Video Settings dialog therefore disables Improved polygon splitting for -Vulkan and explains why. `3D.GL.HiresCoordinates` is different: Vulkan uses it -when producing the polygon edge positions, so Use high resolution coordinates -remains enabled and applies live without rebuilding resolution-sized resources. +Vulkan and explains why. Vulkan also uses high-resolution vertex positions, +but MelonPrimeDS now forces `3D.GL.HiresCoordinates=true`: the checkbox is +checked and disabled, and the backend applies the effective value live without +rebuilding resolution-sized resources. See +[High-resolution vertex coordinates](high-resolution-coordinates.md) for the +cross-backend policy and 1x exception. Per frame, one command buffer: From 6b262558c407e525e15bbb179e8698b57a677553 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 31 Aug 2026 12:52:27 +0900 Subject: [PATCH 3/5] Fix stage expansion settings presentation --- src/frontend/qt_sdl/CMakeLists.txt | 12 ++ .../InputConfig/MelonPrimeInputConfig.ui | 5 +- .../MelonPrimeTranslationsMetroidOptions.inc | 154 +++++++++--------- 3 files changed, 93 insertions(+), 78 deletions(-) diff --git a/src/frontend/qt_sdl/CMakeLists.txt b/src/frontend/qt_sdl/CMakeLists.txt index 56c40f132c..06fc871f93 100644 --- a/src/frontend/qt_sdl/CMakeLists.txt +++ b/src/frontend/qt_sdl/CMakeLists.txt @@ -133,6 +133,18 @@ set(SOURCES_QT_SDL ) +# All three implementation shards include ui_MelonPrimeInputConfig.h. On +# parallel MinGW incremental builds AUTOUIC can refresh that generated header +# after Make has already judged the objects up to date, leaving old UI source +# text paired with a newly compiled localization catalog. Depend on the .ui +# input directly so text, styles, and layout changes always rebuild the users. +set_source_files_properties( + InputConfig/MelonPrimeInputConfig.cpp + InputConfig/MelonPrimeInputConfigConfig.cpp + InputConfig/MelonPrimeInputConfigPreview.cpp + PROPERTIES OBJECT_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/InputConfig/MelonPrimeInputConfig.ui") + option(USE_QT6 "Use Qt 6 instead of Qt 5" ON) option(MELONPRIME_REQUIRE_QT6_5 "Fail configure unless the selected Qt is Qt 6.5 or newer" diff --git a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui index b9a0aedf1f..d1cfcd8e5b 100644 --- a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui +++ b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui @@ -484,12 +484,15 @@ Unlocks 5 stage/mode combinations verified as stable. Applied automatically whil true + + QLabel { margin-left: 16px; } + - Also unlock additional stages + Also unlock additional stages (Not recommended) margin-left: 16px; diff --git a/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidOptions.inc b/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidOptions.inc index 49a7680cb4..1f0159f076 100644 --- a/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidOptions.inc +++ b/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidOptions.inc @@ -2031,84 +2031,84 @@ } }, { - "Also unlock additional stages", + "Also unlock additional stages (Not recommended)", { - {MenuLangId::Japanese, "追加ステージも解放"}, - {MenuLangId::German, "Auch zusätzliche Arenen freischalten"}, - {MenuLangId::Spanish, "Desbloquear también escenarios adicionales"}, - {MenuLangId::French, "Débloquer aussi des arènes supplémentaires"}, - {MenuLangId::Italian, "Sblocca anche arene aggiuntive"}, - {MenuLangId::Dutch, "Ook extra levels ontgrendelen"}, - {MenuLangId::Portuguese, "Também desbloquear fases adicionais"}, - {MenuLangId::Russian, "Также разблокировать дополнительные этапы"}, - {MenuLangId::ChineseSimplified, "同时解锁额外关卡"}, - {MenuLangId::ChineseTraditional, "同時解鎖額外關卡"}, - {MenuLangId::ChineseHongKong, "同時解鎖額外關卡"}, - {MenuLangId::Korean, "추가 스테이지도 잠금 해제"}, - {MenuLangId::Arabic, "فتح مراحل إضافية أيضاً"}, - {MenuLangId::Indonesian, "Buka kunci tahap tambahan juga"}, - {MenuLangId::Ukrainian, "Також розблокувати додаткові етапи"}, - {MenuLangId::Greek, "Ξεκλείδωση επιπλέον σταδίων"}, - {MenuLangId::Swedish, "Lås också upp extra banor"}, - {MenuLangId::Thai, "ปลดล็อกด่านเพิ่มเติมด้วย"}, - {MenuLangId::Czech, "Také odemknout další úrovně"}, - {MenuLangId::Danish, "Lås også yderligere baner op"}, - {MenuLangId::Turkish, "Ek aşamaları da kilidini aç"}, - {MenuLangId::Norwegian, "Lås også opp ekstra baner"}, - {MenuLangId::Hungarian, "További pályák feloldása is"}, - {MenuLangId::Finnish, "Avaa myös lisäkentät"}, - {MenuLangId::Vietnamese, "Cũng mở khóa các màn bổ sung"}, - {MenuLangId::Polish, "Odblokuj także dodatkowe etapy"}, - {MenuLangId::Romanian, "Deblochează și etape suplimentare"}, - {MenuLangId::Filipino, "Buksan din ang karagdagang arena"}, - {MenuLangId::Swahili, "Fungua pia viwanja vya ziada"}, - {MenuLangId::Croatian, "Također otključaj dodatne arene"}, - {MenuLangId::Icelandic, "Opna einnig fleiri velli"}, - {MenuLangId::Hindi, "अतिरिक्त मैदान भी खोलें"}, - {MenuLangId::Bengali, "অতিরিক্ত ম্যাপও খুলুন"}, - {MenuLangId::Urdu, "اضافی میدان بھی کھولیں"}, - {MenuLangId::Punjabi, "ਵਾਧੂ ਮੈਦਾਨ ਵੀ ਖੋਲ੍ਹੋ"}, - {MenuLangId::Telugu, "అదనపు అరేనాలను కూడా తెరువు"}, - {MenuLangId::Persian, "Stageهای اضافی را هم باز کن"}, - {MenuLangId::Malay, "Buka arena tambahan juga"}, - {MenuLangId::Marathi, "अतिरिक्त मैदानही उघडा"}, - {MenuLangId::Tamil, "கூடுதல் அரங்குகளையும் திற"}, - {MenuLangId::Gujarati, "વધારાના મેદાન પણ ખોલો"}, - {MenuLangId::Kannada, "ಹೆಚ್ಚುವರಿ ಅರೇನಾಗಳನ್ನೂ ತೆರೆಯಿರಿ"}, - {MenuLangId::Odia, "ଅତିରିକ୍ତ ମଞ୍ଚମାନଙ୍କୁ ମଧ୍ୟ ଖୋଲନ୍ତୁ"}, - {MenuLangId::Malayalam, "കൂടുതൽ അരങ്ങുകളും തുറക്കുക"}, - {MenuLangId::Burmese, "အပိုကွင်းများကိုလည်း ဖွင့်ပါ"}, - {MenuLangId::Uzbek, "Qoʻshimcha arenalarni ham ochish"}, - {MenuLangId::Azerbaijani, "Əlavə arenaları da aç"}, - {MenuLangId::Nepali, "अतिरिक्त मैदानहरू पनि खोल्नुहोस्"}, - {MenuLangId::Sinhala, "අමතර අරීනා ද විවෘත කරන්න"}, - {MenuLangId::Khmer, "បើកសង្វៀនបន្ថែមផងដែរ"}, - {MenuLangId::Lao, "ເປີດສະໜາມເພີ່ມດ້ວຍ"}, - {MenuLangId::Armenian, "Բացել նաև լրացուցիչ ասպարեզները"}, - {MenuLangId::Albanian, "Hap edhe arena shtesë"}, - {MenuLangId::Georgian, "დამატებითი არენებიც გახსენი"}, - {MenuLangId::Bosnian, "Otključaj i dodatne arene"}, - {MenuLangId::Serbian, "Откључај и додатне арене"}, - {MenuLangId::Macedonian, "Отклучи и дополнителни арени"}, - {MenuLangId::Bulgarian, "Отключи и допълнителни арени"}, - {MenuLangId::Belarusian, "Адкрыць і дадатковыя арэны"}, - {MenuLangId::Maltese, "Iftaħ ukoll areni addizzjonali"}, - {MenuLangId::Mongolian, "Нэмэлт талбайг мөн нээх"}, - {MenuLangId::Latvian, "Atvērt arī papildu arēnas"}, - {MenuLangId::Lithuanian, "Atrakinti ir papildomas arenas"}, - {MenuLangId::Afrikaans, "Ontsluit ook ekstra arenas"}, - {MenuLangId::Irish, "Díghlasáil ardáin bhreise freisin"}, - {MenuLangId::Assamese, "অতিৰিক্ত মঞ্চসমূহো খোলক"}, - {MenuLangId::Amharic, "ተጨማሪ መድረኮችንም ክፈት"}, - {MenuLangId::Estonian, "Ava ka lisaareenid"}, - {MenuLangId::Kazakh, "Қосымша алаңдарды да ашу"}, - {MenuLangId::Catalan, "Desbloqueja també arenes addicionals"}, - {MenuLangId::Kyrgyz, "Кошумча аянтчаларды да ачуу"}, - {MenuLangId::Slovak, "Odomknúť aj ďalšie arény"}, - {MenuLangId::Slovenian, "Odkleni tudi dodatne arene"}, - {MenuLangId::Zulu, "Vula nezinkundla ezengeziwe"}, - {MenuLangId::Basque, "Ireki areta gehigarriak ere"}, - {MenuLangId::Hebrew, "פתח גם זירות נוספות"}, + {MenuLangId::Japanese, "追加ステージも解放(非推奨)"}, + {MenuLangId::German, "Auch zusätzliche Arenen freischalten (nicht empfohlen)"}, + {MenuLangId::Spanish, "Desbloquear también escenarios adicionales (no recomendado)"}, + {MenuLangId::French, "Débloquer aussi des arènes supplémentaires (non recommandé)"}, + {MenuLangId::Italian, "Sblocca anche arene aggiuntive (non consigliato)"}, + {MenuLangId::Dutch, "Ook extra levels ontgrendelen (niet aanbevolen)"}, + {MenuLangId::Portuguese, "Também desbloquear fases adicionais (não recomendado)"}, + {MenuLangId::Russian, "Также разблокировать дополнительные этапы (не рекомендуется)"}, + {MenuLangId::ChineseSimplified, "同时解锁额外关卡(不推荐)"}, + {MenuLangId::ChineseTraditional, "同時解鎖額外關卡(不推薦)"}, + {MenuLangId::ChineseHongKong, "同時解鎖額外關卡(不推薦)"}, + {MenuLangId::Korean, "추가 스테이지도 잠금 해제 (권장하지 않음)"}, + {MenuLangId::Arabic, "فتح مراحل إضافية أيضاً (غير موصى به)"}, + {MenuLangId::Indonesian, "Buka kunci tahap tambahan juga (tidak disarankan)"}, + {MenuLangId::Ukrainian, "Також розблокувати додаткові етапи (не рекомендується)"}, + {MenuLangId::Greek, "Ξεκλείδωση επιπλέον σταδίων (δεν συνιστάται)"}, + {MenuLangId::Swedish, "Lås också upp extra banor (rekommenderas inte)"}, + {MenuLangId::Thai, "ปลดล็อกด่านเพิ่มเติมด้วย (ไม่แนะนำ)"}, + {MenuLangId::Czech, "Také odemknout další úrovně (nedoporučeno)"}, + {MenuLangId::Danish, "Lås også yderligere baner op (anbefales ikke)"}, + {MenuLangId::Turkish, "Ek aşamaları da kilidini aç (önerilmez)"}, + {MenuLangId::Norwegian, "Lås også opp ekstra baner (anbefales ikke)"}, + {MenuLangId::Hungarian, "További pályák feloldása is (nem ajánlott)"}, + {MenuLangId::Finnish, "Avaa myös lisäkentät (ei suositella)"}, + {MenuLangId::Vietnamese, "Cũng mở khóa các màn bổ sung (không khuyến nghị)"}, + {MenuLangId::Polish, "Odblokuj także dodatkowe etapy (niezalecane)"}, + {MenuLangId::Romanian, "Deblochează și etape suplimentare (nerecomandat)"}, + {MenuLangId::Filipino, "Buksan din ang karagdagang arena (hindi inirerekomenda)"}, + {MenuLangId::Swahili, "Fungua pia viwanja vya ziada (haipendekezwi)"}, + {MenuLangId::Croatian, "Također otključaj dodatne arene (nije preporučeno)"}, + {MenuLangId::Icelandic, "Opna einnig fleiri velli (ekki mælt með)"}, + {MenuLangId::Hindi, "अतिरिक्त मैदान भी खोलें (अनुशंसित नहीं)"}, + {MenuLangId::Bengali, "অতিরিক্ত ম্যাপও খুলুন (প্রস্তাবিত নয়)"}, + {MenuLangId::Urdu, "اضافی میدان بھی کھولیں (تجویز کردہ نہیں)"}, + {MenuLangId::Punjabi, "ਵਾਧੂ ਮੈਦਾਨ ਵੀ ਖੋਲ੍ਹੋ (ਸਿਫ਼ਾਰਸ਼ ਨਹੀਂ ਕੀਤੀ ਜਾਂਦੀ)"}, + {MenuLangId::Telugu, "అదనపు అరేనాలను కూడా తెరువు (సిఫార్సు చేయబడదు)"}, + {MenuLangId::Persian, "Stageهای اضافی را هم باز کن (توصیه نمی‌شود)"}, + {MenuLangId::Malay, "Buka arena tambahan juga (tidak disyorkan)"}, + {MenuLangId::Marathi, "अतिरिक्त मैदानही उघडा (शिफारस केलेली नाही)"}, + {MenuLangId::Tamil, "கூடுதல் அரங்குகளையும் திற (பரிந்துரைக்கப்படவில்லை)"}, + {MenuLangId::Gujarati, "વધારાના મેદાન પણ ખોલો (ભલામણ કરેલ નથી)"}, + {MenuLangId::Kannada, "ಹೆಚ್ಚುವರಿ ಅರೇನಾಗಳನ್ನೂ ತೆರೆಯಿರಿ (ಶಿಫಾರಸು ಮಾಡಲಾಗುವುದಿಲ್ಲ)"}, + {MenuLangId::Odia, "ଅତିରିକ୍ତ ମଞ୍ଚମାନଙ୍କୁ ମଧ୍ୟ ଖୋଲନ୍ତୁ (ସୁପାରିଶ ନୁହେଁ)"}, + {MenuLangId::Malayalam, "കൂടുതൽ അരങ്ങുകളും തുറക്കുക (ശുപാർശ ചെയ്യുന്നില്ല)"}, + {MenuLangId::Burmese, "အပိုကွင်းများကိုလည်း ဖွင့်ပါ (မထောက်ခံပါ)"}, + {MenuLangId::Uzbek, "Qoʻshimcha arenalarni ham ochish (tavsiya etilmaydi)"}, + {MenuLangId::Azerbaijani, "Əlavə arenaları da aç (tövsiyə edilmir)"}, + {MenuLangId::Nepali, "अतिरिक्त मैदानहरू पनि खोल्नुहोस् (सिफारिस गरिएको छैन)"}, + {MenuLangId::Sinhala, "අමතර අරීනා ද විවෘත කරන්න (නිර්දේශ නොකරයි)"}, + {MenuLangId::Khmer, "បើកសង្វៀនបន្ថែមផងដែរ (មិនណែនាំ)"}, + {MenuLangId::Lao, "ເປີດສະໜາມເພີ່ມດ້ວຍ (ບໍ່ແນະນໍາ)"}, + {MenuLangId::Armenian, "Բացել նաև լրացուցիչ ասպարեզները (խորհուրդ չի տրվում)"}, + {MenuLangId::Albanian, "Hap edhe arena shtesë (nuk rekomandohet)"}, + {MenuLangId::Georgian, "დამატებითი არენებიც გახსენი (არ არის რეკომენდებული)"}, + {MenuLangId::Bosnian, "Otključaj i dodatne arene (nije preporučljivo)"}, + {MenuLangId::Serbian, "Откључај и додатне арене (није препоручљиво)"}, + {MenuLangId::Macedonian, "Отклучи и дополнителни арени (не се препорачува)"}, + {MenuLangId::Bulgarian, "Отключи и допълнителни арени (не се препоръчва)"}, + {MenuLangId::Belarusian, "Адкрыць і дадатковыя арэны (не рэкамендуецца)"}, + {MenuLangId::Maltese, "Iftaħ ukoll areni addizzjonali (mhux rakkomandat)"}, + {MenuLangId::Mongolian, "Нэмэлт талбайг мөн нээх (зөвлөдөггүй)"}, + {MenuLangId::Latvian, "Atvērt arī papildu arēnas (nav ieteicams)"}, + {MenuLangId::Lithuanian, "Atrakinti ir papildomas arenas (nerekomenduojama)"}, + {MenuLangId::Afrikaans, "Ontsluit ook ekstra arenas (nie aanbeveel nie)"}, + {MenuLangId::Irish, "Díghlasáil ardáin bhreise freisin (ní mholtar)"}, + {MenuLangId::Assamese, "অতিৰিক্ত মঞ্চসমূহো খোলক (পৰামৰ্শ দিয়া নহয়)"}, + {MenuLangId::Amharic, "ተጨማሪ መድረኮችንም ክፈት (አይመከርም)"}, + {MenuLangId::Estonian, "Ava ka lisaareenid (ei ole soovitatav)"}, + {MenuLangId::Kazakh, "Қосымша алаңдарды да ашу (ұсынылмайды)"}, + {MenuLangId::Catalan, "Desbloqueja també arenes addicionals (no recomanat)"}, + {MenuLangId::Kyrgyz, "Кошумча аянтчаларды да ачуу (сунушталбайт)"}, + {MenuLangId::Slovak, "Odomknúť aj ďalšie arény (neodporúča sa)"}, + {MenuLangId::Slovenian, "Odkleni tudi dodatne arene (ni priporočljivo)"}, + {MenuLangId::Zulu, "Vula nezinkundla ezengeziwe (akunconywa)"}, + {MenuLangId::Basque, "Ireki areta gehigarriak ere (ez da gomendatzen)"}, + {MenuLangId::Hebrew, "פתח גם זירות נוספות (לא מומלץ)"}, } }, { From ea6fdf9ded2dd0d9d629bf76fa10ae555d5cdfcb Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 31 Aug 2026 12:59:20 +0900 Subject: [PATCH 4/5] Reorder stylus cursor settings --- .../InputConfig/MelonPrimeInputConfig.cpp | 4 +-- .../InputConfig/MelonPrimeInputConfig.ui | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp index 82ac31a026..480a6980e5 100644 --- a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp +++ b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp @@ -1352,12 +1352,12 @@ void MelonPrimeInputConfig::setupSettingsOrganization() detachWidget(ui->lblMetroidJoy2KeySupportDesc); appendInputWidget(ui->cbMetroidEnableStylusMode); + appendInputWidget(ui->cbMetroidEnableStylusHoldCursorAtCenterWhenNotClicking); + appendInputWidget(ui->cbMetroidEnableStylusHideCursorInGame); appendInputWidget(ui->cbMetroidEnableTouchScreenAimOnly); appendInputWidget(ui->cbMetroidEnableTouchScreenAimOnlySuspendForTransform); appendInputWidget(ui->cbMetroidEnableTopScreenTouch); - appendInputWidget(ui->cbMetroidEnableStylusHideCursorInGame); appendInputWidget(ui->cbMetroidEnableStylusConfineCursorToTopScreen); - appendInputWidget(ui->cbMetroidEnableStylusHoldCursorAtCenterWhenNotClicking); appendSeparator(); appendInputWidget(m_cbMetroidDisableMorphBoostSwipe); diff --git a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui index d1cfcd8e5b..5b2e7fb505 100644 --- a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui +++ b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui @@ -145,38 +145,38 @@ - - Use the Whole Touch Screen for Aiming (Disables the touch-screen HUD buttons: Transform, weapon quick slots and the weapon menu. Double-tap jump and the touch boost gesture still work) + + Hold the Cursor at the Center While No Click Is Held During a Match in Stylus Mode (every drag then starts from the center with its full range) QCheckBox { margin-left: 24px; } - - Turn It Off Briefly While a Standard-Method Transform Fires (The Standard transform taps the touch-screen Transform button, which the option above disables. This re-enables the HUD hit-test for those few frames only) - QCheckBox { margin-left: 44px; } + + Hide the Cursor During Gameplay Even in Stylus Mode (Presentation only: the pointer stays free, so stylus aiming and touch input are unaffected) + QCheckBox { margin-left: 24px; } - - Hide the Cursor During Gameplay Even in Stylus Mode (Presentation only: the pointer stays free, so stylus aiming and touch input are unaffected) + + Use the Whole Touch Screen for Aiming (Disables the touch-screen HUD buttons: Transform, weapon quick slots and the weapon menu. Double-tap jump and the touch boost gesture still work) QCheckBox { margin-left: 24px; } - - Enable Touch Input on the Top Screen - QCheckBox { margin-left: 24px; } + + Turn It Off Briefly While a Standard-Method Transform Fires (The Standard transform taps the touch-screen Transform button, which the option above disables. This re-enables the HUD hit-test for those few frames only) + QCheckBox { margin-left: 44px; } - - Confine the Cursor to the Top Screen During a Match in Stylus Mode (Windows only) + + Enable Touch Input on the Top Screen QCheckBox { margin-left: 24px; } - - Hold the Cursor at the Center While No Click Is Held During a Match in Stylus Mode (every drag then starts from the center with its full range) + + Confine the Cursor to the Top Screen During a Match in Stylus Mode (Windows only) QCheckBox { margin-left: 24px; } From 15a0decad51c48adb677db334c7789ed614ec776 Mon Sep 17 00:00:00 2001 From: Zection6V Date: Mon, 31 Aug 2026 15:42:04 +0900 Subject: [PATCH 5/5] Add stylus-mode direct mouse aim --- src/frontend/qt_sdl/Config.cpp | 7 + src/frontend/qt_sdl/EmuInstance.h | 8 + src/frontend/qt_sdl/EmuInstanceInput.cpp | 16 ++ .../InputConfig/MelonPrimeInputConfig.cpp | 33 +++- .../InputConfig/MelonPrimeInputConfig.h | 3 + .../InputConfig/MelonPrimeInputConfig.ui | 34 ++-- src/frontend/qt_sdl/MelonPrime.cpp | 11 +- src/frontend/qt_sdl/MelonPrime.h | 16 +- src/frontend/qt_sdl/MelonPrimeDef.h | 1 + src/frontend/qt_sdl/MelonPrimeGameInput.cpp | 43 +++-- src/frontend/qt_sdl/MelonPrimeInGame.cpp | 32 +++- .../qt_sdl/MelonPrimeInputProjection.h | 83 ++++++++- src/frontend/qt_sdl/MelonPrimeLifecycle.cpp | 6 +- .../inc/MelonPrimeTranslationsHotkeys.inc | 162 ++++++++++++++++ .../MelonPrimeTranslationsMetroidGameplay.inc | 81 ++++++++ .../MelonPrimePatchLowLatencyAimHook.inc | 4 +- ...NativeAimDeltaHookPostFoldWriteVersion.inc | 8 +- ...veAimDeltaHookRegisterInjectionVersion.inc | 10 +- .../qt_sdl/MelonPrimeRawHotkeyVkBinding.cpp | 7 +- .../qt_sdl/MelonPrimeRuntimeConfig.cpp | 2 + src/frontend/qt_sdl/MelonPrimeRuntimeConfig.h | 3 + src/frontend/qt_sdl/MelonPrimeThreadBridge.h | 22 +++ src/frontend/qt_sdl/Screen.cpp | 173 +++++++++++++++++- src/frontend/qt_sdl/Screen.h | 14 ++ src/frontend/qt_sdl/Window.cpp | 16 ++ 25 files changed, 727 insertions(+), 68 deletions(-) diff --git a/src/frontend/qt_sdl/Config.cpp b/src/frontend/qt_sdl/Config.cpp index c6809188d9..ed4c37f533 100644 --- a/src/frontend/qt_sdl/Config.cpp +++ b/src/frontend/qt_sdl/Config.cpp @@ -132,7 +132,9 @@ namespace Config {"Instance*.Keyboard.HK_MetroidUIYes", Qt::Key_G}, {"Instance*.Keyboard.HK_MetroidUINo", Qt::Key_H}, {"Instance*.Keyboard.HK_MetroidShootScan", (int)Qt::LeftButton | (int)0xF0000000}, + {"Instance*.Keyboard.HK_MetroidStylusTouch", (int)Qt::LeftButton | (int)0xF0000000}, {"Instance*.Keyboard.HK_MetroidScanShoot", Qt::Key_V}, + {"Instance*.Keyboard.HK_MetroidScanShootStylus", Qt::Key_V}, {"Instance*.Keyboard.HK_MetroidWeaponBeam", (int)Qt::ExtraButton2 | (int)0xF0000000}, {"Instance*.Keyboard.HK_MetroidWeaponMissile", (int)Qt::ExtraButton1 | (int)0xF0000000}, {"Instance*.Keyboard.HK_MetroidWeaponSpecial", Qt::Key_R}, @@ -263,6 +265,7 @@ namespace Config {"Instance*.Metroid.Apply.MusicVolume", false}, {"Instance*.Metroid.Apply.joy2KeySupport", true}, {"Instance*.Metroid.Enable.stylusMode", false}, + {"Instance*.Metroid.Enable.stylusDirectAimWhileTouching", false}, {"Instance*.Metroid.Enable.topScreenTouch", false}, {"Instance*.Metroid.Enable.touchScreenAimOnly", false}, {"Instance*.Metroid.Enable.touchScreenAimOnlySuspendForTransform", true}, @@ -448,7 +451,9 @@ namespace Config {"HKKey_MetroidUINo", 0, "Keyboard.HK_MetroidUINo", true}, {"HKKey_MetroidShootScan", 0, "Keyboard.HK_MetroidShootScan", true}, + {"HKKey_MetroidStylusTouch", 0, "Keyboard.HK_MetroidStylusTouch", true}, {"HKKey_MetroidScanShoot", 0, "Keyboard.HK_MetroidScanShoot", true}, + {"HKKey_MetroidScanShootStylus", 0, "Keyboard.HK_MetroidScanShootStylus", true}, {"HKKey_MetroidWeaponBeam", 0, "Keyboard.HK_MetroidWeaponBeam", true}, {"HKKey_MetroidWeaponMissile", 0, "Keyboard.HK_MetroidWeaponMissile", true}, @@ -515,7 +520,9 @@ namespace Config { "HKJoy_MetroidUINo", 0, "Joystick.HK_MetroidUINo", true}, { "HKJoy_MetroidShootScan", 0, "Joystick.HK_MetroidShootScan", true}, + { "HKJoy_MetroidStylusTouch", 0, "Joystick.HK_MetroidStylusTouch", true}, { "HKJoy_MetroidScanShoot", 0, "Joystick.HK_MetroidScanShoot", true}, + { "HKJoy_MetroidScanShootStylus", 0, "Joystick.HK_MetroidScanShootStylus", true}, { "HKJoy_MetroidWeaponBeam", 0, "Joystick.HK_MetroidWeaponBeam", true}, { "HKJoy_MetroidWeaponMissile", 0, "Joystick.HK_MetroidWeaponMissile", true}, diff --git a/src/frontend/qt_sdl/EmuInstance.h b/src/frontend/qt_sdl/EmuInstance.h index c70acc4dcc..e45f67ccfb 100755 --- a/src/frontend/qt_sdl/EmuInstance.h +++ b/src/frontend/qt_sdl/EmuInstance.h @@ -99,6 +99,12 @@ enum // packing stays valid. HK_MetroidWeaponNextSecondary, HK_MetroidWeaponPreviousSecondary, + // Appended to preserve every existing hotkey ID. Runtime projection selects + // this binding only while Stylus Mode is enabled. + HK_MetroidScanShootStylus, + // Appended to preserve every existing hotkey ID. In Stylus Mode this + // binding drives touch contact; outside Stylus Mode it is ignored. + HK_MetroidStylusTouch, #endif // MELONPRIME_DS // HK_MAX should be last item. @@ -170,6 +176,8 @@ class EmuInstance #ifdef MELONPRIME_DS void onMousePress(QMouseEvent* event); void onMouseRelease(QMouseEvent* event); + [[nodiscard]] bool hotkeyUsesKeyboardKey(int hotkeyId, int qtKey) const; + [[nodiscard]] bool hotkeyUsesMouseButton(int hotkeyId, Qt::MouseButton button) const; void syncMouseHotkeysFromQtButtons(Qt::MouseButtons physical); // One-frame virtual press for MelonPrime::InputKey::MouseWheelUp/Down bindings. void onMouseWheel(int delta); diff --git a/src/frontend/qt_sdl/EmuInstanceInput.cpp b/src/frontend/qt_sdl/EmuInstanceInput.cpp index f1a3059c69..9b85fb0d68 100644 --- a/src/frontend/qt_sdl/EmuInstanceInput.cpp +++ b/src/frontend/qt_sdl/EmuInstanceInput.cpp @@ -109,6 +109,8 @@ const char* EmuInstance::hotkeyNames[HK_MAX] = "HK_MetroidIngameSensiDown", "HK_MetroidWeaponNextSecondary", "HK_MetroidWeaponPreviousSecondary", + "HK_MetroidScanShootStylus", + "HK_MetroidStylusTouch", #endif }; @@ -463,6 +465,20 @@ void EmuInstance::onMouseRelease(QMouseEvent* event) keyHotkeyMask.fetch_and(~(1ULL << i), std::memory_order_relaxed); } +bool EmuInstance::hotkeyUsesKeyboardKey(int hotkeyId, int qtKey) const +{ + return hotkeyId >= 0 && hotkeyId < HK_MAX + && hkKeyMapping[hotkeyId] == qtKey; +} + +bool EmuInstance::hotkeyUsesMouseButton(int hotkeyId, Qt::MouseButton button) const +{ + if (hotkeyId < 0 || hotkeyId >= HK_MAX) + return false; + const int key = static_cast(button) | MelonPrime::InputKey::MouseMark; + return hkKeyMapping[hotkeyId] == key; +} + void EmuInstance::syncMouseHotkeysFromQtButtons(Qt::MouseButtons physical) { auto releaseIfUp = [&](Qt::MouseButton btn) { diff --git a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp index 480a6980e5..46c1893c8e 100644 --- a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp +++ b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.cpp @@ -519,6 +519,10 @@ void MelonPrimeInputConfig::buildSettingBindings() // index stays stable; only the tail load range grows. { C::TouchScreenAimOnlySuspendForTransform, K::CheckBool, ui->cbMetroidEnableTouchScreenAimOnlySuspendForTransform }, // 54 + // Direct relative aim remains a Stylus Mode sub-option. Appended so + // every historical binding index and its observable load order stay put. + { C::StylusDirectAimWhileTouching, K::CheckBool, + ui->cbMetroidEnableStylusDirectAimWhileTouching }, // 55 }; } @@ -736,7 +740,7 @@ void MelonPrimeInputConfig::setupSensitivityAndToggles(Config::Table& instcfg) // V15 appended bindings 45..47: distance, inverted disable parent, custom mode. // Mouse-wheel weapon cycling is now Next/Previous Weapon (Secondary) bindings. loadBindingsRange(instcfg, 45, 48); // MELONPRIME_DISABLE_CHECKBOX_SEMANTICS_V15 - loadBindingsRange(instcfg, 49, 55); // top-screen touch, touch-screen aim-only (+ its sub-option), stylus cursor options + loadBindingsRange(instcfg, 49, 56); // top-screen touch, touch-screen aim-only (+ sub-options), stylus cursor/direct-aim options if (!instcfg.HasKey(MelonPrime::CfgKey::MorphBoostSwipeEnabled) && instcfg.GetInt(MelonPrime::CfgKey::MorphBoostSwipeDistance) <= 0) { m_cbMetroidDisableMorphBoostSwipe->setChecked(true); @@ -1352,6 +1356,7 @@ void MelonPrimeInputConfig::setupSettingsOrganization() detachWidget(ui->lblMetroidJoy2KeySupportDesc); appendInputWidget(ui->cbMetroidEnableStylusMode); + appendInputWidget(ui->cbMetroidEnableStylusDirectAimWhileTouching); appendInputWidget(ui->cbMetroidEnableStylusHoldCursorAtCenterWhenNotClicking); appendInputWidget(ui->cbMetroidEnableStylusHideCursorInGame); appendInputWidget(ui->cbMetroidEnableTouchScreenAimOnly); @@ -1494,16 +1499,25 @@ void MelonPrimeInputConfig::updateAimControlsForStylusMode(bool stylusEnabled) { // Stylus-specific options retain their saved check state, but cannot be // edited until Stylus Mode itself is enabled. - ui->cbMetroidEnableTouchScreenAimOnly->setEnabled(stylusEnabled); - // Sub-option: only meaningful while the parent patch is actually on. + const bool directAimEnabled = stylusEnabled + && ui->cbMetroidEnableStylusDirectAimWhileTouching->isChecked(); + const bool absoluteTouchAimEnabled = stylusEnabled && !directAimEnabled; + // These options only affect the traditional absolute DS-touch aim route. + // Keep their saved values intact while the direct relative route is active. + ui->cbMetroidEnableTouchScreenAimOnly->setEnabled(absoluteTouchAimEnabled); + // Sub-option: only meaningful while its parent patch and the traditional + // absolute-touch route are both active. ui->cbMetroidEnableTouchScreenAimOnlySuspendForTransform->setEnabled( - stylusEnabled && ui->cbMetroidEnableTouchScreenAimOnly->isChecked()); + absoluteTouchAimEnabled + && ui->cbMetroidEnableTouchScreenAimOnly->isChecked()); ui->cbMetroidEnableStylusHideCursorInGame->setEnabled(stylusEnabled); - ui->cbMetroidEnableTopScreenTouch->setEnabled(stylusEnabled); + ui->cbMetroidEnableTopScreenTouch->setEnabled(absoluteTouchAimEnabled); ui->cbMetroidEnableStylusConfineCursorToTopScreen->setEnabled(stylusEnabled); - ui->cbMetroidEnableStylusHoldCursorAtCenterWhenNotClicking->setEnabled(stylusEnabled); + ui->cbMetroidEnableStylusHoldCursorAtCenterWhenNotClicking->setEnabled( + absoluteTouchAimEnabled); + ui->cbMetroidEnableStylusDirectAimWhileTouching->setEnabled(stylusEnabled); - const bool enableAimControls = !stylusEnabled; + const bool enableAimControls = !stylusEnabled || directAimEnabled; ui->metroidAimSensitvitySpinBox->setEnabled(enableAimControls); ui->metroidAimSensitvityLabel->setEnabled(enableAimControls); ui->metroidAimYAxisScaleSpinBox->setEnabled(enableAimControls); @@ -1894,6 +1908,11 @@ void MelonPrimeInputConfig::on_cbMetroidEnableStylusMode_stateChanged(int state) updateAimControlsForStylusMode(state != 0); } +void MelonPrimeInputConfig::on_cbMetroidEnableStylusDirectAimWhileTouching_stateChanged(int) +{ + updateAimControlsForStylusMode(ui->cbMetroidEnableStylusMode->isChecked()); +} + void MelonPrimeInputConfig::on_cbMetroidEnableTouchScreenAimOnly_stateChanged(int) { // Refresh the sub-option, which is only meaningful while this one is on. diff --git a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.h b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.h index 99acc2b74b..701ec95556 100644 --- a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.h +++ b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.h @@ -45,7 +45,9 @@ static constexpr HotkeyEntry kMetroidHotkeys[] = {HK_MetroidMoveLeft, "[Metroid] (A) Move Left"}, {HK_MetroidMoveRight, "[Metroid] (D) Move Right"}, {HK_MetroidShootScan, "[Metroid] (Mouse Left) Shoot/Scan"}, + {HK_MetroidStylusTouch, "[Metroid] (Mouse Left) Touch (Aim), Double-Tap Jump (Stylus Mode)"}, {HK_MetroidScanShoot, "[Metroid] (V) Scan/Shoot, Map Zoom In"}, + {HK_MetroidScanShootStylus, "[Metroid] (V) Scan/Shoot, Map Zoom In (Stylus Mode)"}, {HK_MetroidZoom, "[Metroid] (Mouse Right) Imperialist Zoom, Map Zoom Out, Morph Ball Boost"}, {HK_MetroidJump, "[Metroid] (Space) Jump"}, {HK_MetroidMorphBall, "[Metroid] (L. Ctrl) Transform"}, @@ -122,6 +124,7 @@ private slots: void on_cbMetroidHudCrosshairDeadbandEnable_stateChanged(int state); void on_dsbMetroidHudCrosshairDeadband_valueChanged(double value); void on_cbMetroidEnableStylusMode_stateChanged(int state); + void on_cbMetroidEnableStylusDirectAimWhileTouching_stateChanged(int state); void on_cbMetroidEnableTouchScreenAimOnly_stateChanged(int state); void on_cbMetroidDisableMphAimSmoothing_stateChanged(int state); void on_cbMetroidExpandStageMatrix_stateChanged(int state); diff --git a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui index 5b2e7fb505..a316e27b90 100644 --- a/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui +++ b/src/frontend/qt_sdl/InputConfig/MelonPrimeInputConfig.ui @@ -145,42 +145,48 @@ + + Use Normal Mouse Aim While the Stylus Touch Key Is Held (Detects cursor movement and writes aim directly instead of aiming through DS touch input) + QCheckBox { margin-left: 24px; } + + + Hold the Cursor at the Center While No Click Is Held During a Match in Stylus Mode (every drag then starts from the center with its full range) QCheckBox { margin-left: 24px; } - + Hide the Cursor During Gameplay Even in Stylus Mode (Presentation only: the pointer stays free, so stylus aiming and touch input are unaffected) QCheckBox { margin-left: 24px; } - + Use the Whole Touch Screen for Aiming (Disables the touch-screen HUD buttons: Transform, weapon quick slots and the weapon menu. Double-tap jump and the touch boost gesture still work) QCheckBox { margin-left: 24px; } - + Turn It Off Briefly While a Standard-Method Transform Fires (The Standard transform taps the touch-screen Transform button, which the option above disables. This re-enables the HUD hit-test for those few frames only) QCheckBox { margin-left: 44px; } - + Enable Touch Input on the Top Screen QCheckBox { margin-left: 24px; } - + Confine the Cursor to the Top Screen During a Match in Stylus Mode (Windows only) QCheckBox { margin-left: 24px; } - + 0 @@ -193,14 +199,14 @@ - + Aim sensitivity (default: 63) - + 0.000001 @@ -219,14 +225,14 @@ - + Aim Y-Axis Scale (default: 1.5147) - + (1.5147 = MPH native; option: 1.9429 = perfectly matches X/Y angular velocity) @@ -239,7 +245,7 @@ - + 0.000000 @@ -258,19 +264,19 @@ - + <html><head/><body><p><b>Input threshold (deadzone + snap)</b> (recommended: 0.01 [default] or 0.5): |x|&lt;a → 0, a ≤ |x|&lt;1 → ±1, |x|≥1 → as is (a=0: off = all values under 1 ignored, no snap)<br/>When your aim isn't going the way you want, try lowering this value.</p></body></html> - + Enable Aim Sub-pixel Accumulator (Carry fractional mouse movement across frames. Enable for smoother low-sensitivity aiming) - + Reset sensitivity values diff --git a/src/frontend/qt_sdl/MelonPrime.cpp b/src/frontend/qt_sdl/MelonPrime.cpp index 25eb97c842..1177e7fbf3 100644 --- a/src/frontend/qt_sdl/MelonPrime.cpp +++ b/src/frontend/qt_sdl/MelonPrime.cpp @@ -283,8 +283,15 @@ namespace MelonPrime { const bool isStylusMode = this->isStylusMode; if (isStylusMode) { - if (emuInstance->isTouching && !m_flags.test(StateFlags::BIT_BLOCK_STYLUS)) { - emuInstance->getNDS()->TouchScreen(emuInstance->touchX, emuInstance->touchY); + if (!m_flags.test(StateFlags::BIT_BLOCK_STYLUS)) { + if (m_enableStylusDirectAimWhileTouching + && m_stylusTouchKeyDown) + { + ProcessAimInputMouse(); + } + else { + ProcessAimInputStylus(emuInstance->getNDS()); + } } } else { diff --git a/src/frontend/qt_sdl/MelonPrime.h b/src/frontend/qt_sdl/MelonPrime.h index 5a88778442..2404cc2f2e 100644 --- a/src/frontend/qt_sdl/MelonPrime.h +++ b/src/frontend/qt_sdl/MelonPrime.h @@ -888,11 +888,19 @@ namespace MelonPrime { // empty on a normal frame (~40–100 ns window). Skip the // GetRawInputBuffer syscall entirely (~500–2000 cyc saved). bool m_didFrameAdvanceSinceSnapshot = false; - // True when the V-default ScanShoot key (HK_MetroidScanShoot) is held this - // frame. Used to keep the shoot/scan/map-expand input working during the - // Adventure map/user-action pause while the Mouse-Left ShootScan key stays - // touch-only (a left click must not fire there). Set in UpdateInputStateImpl. + // True when the active mode-specific ScanShoot key is held this frame. + // Used to keep the shoot/scan/map-expand input working during the + // Adventure map/user-action pause while the Mouse-Left ShootScan key + // stays touch-only. Set in UpdateInputStateImpl. bool m_scanShootKeyDown = false; + // The configurable Stylus Mode touch-contact action. This remains + // separate from IB_SHOOT: the game itself turns touch edges into aim, + // firing, and double-tap jump behavior. + bool m_stylusTouchKeyDown = false; + // Selects the normal relative-mouse/direct-write aim producer while + // Stylus Mode's configurable touch action is held. Loaded only at the + // cold runtime-config boundary. + bool m_enableStylusDirectAimWhileTouching = false; // Loaded only at the cold config boundary. Vulkan reads this scalar // before every frame so an F8 load cannot expose one unmasked frame // while the ARM patch tracker is still being re-established. diff --git a/src/frontend/qt_sdl/MelonPrimeDef.h b/src/frontend/qt_sdl/MelonPrimeDef.h index e00ee4431b..038c88032a 100644 --- a/src/frontend/qt_sdl/MelonPrimeDef.h +++ b/src/frontend/qt_sdl/MelonPrimeDef.h @@ -37,6 +37,7 @@ namespace MelonPrime { inline constexpr const char* Joy2Key = "Metroid.Apply.joy2KeySupport"; inline constexpr const char* SnapTap = "Metroid.Operation.SnapTap"; inline constexpr const char* StylusMode = "Metroid.Enable.stylusMode"; + inline constexpr const char* StylusDirectAimWhileTouching = "Metroid.Enable.stylusDirectAimWhileTouching"; inline constexpr const char* TopScreenTouch = "Metroid.Enable.topScreenTouch"; inline constexpr const char* TouchScreenAimOnly = "Metroid.Enable.touchScreenAimOnly"; inline constexpr const char* TouchScreenAimOnlySuspendForTransform = "Metroid.Enable.touchScreenAimOnlySuspendForTransform"; diff --git a/src/frontend/qt_sdl/MelonPrimeGameInput.cpp b/src/frontend/qt_sdl/MelonPrimeGameInput.cpp index 276cacdb10..34554a826b 100644 --- a/src/frontend/qt_sdl/MelonPrimeGameInput.cpp +++ b/src/frontend/qt_sdl/MelonPrimeGameInput.cpp @@ -76,10 +76,17 @@ namespace MelonPrime { m_threadBridge.ReadCenterForEmu( m_aimData.centerX, m_aimData.centerY); } - const bool captureEligible = focused - && m_threadBridge.PanelAvailableForEmu() - && !isCursorMode - && m_threadBridge.CaptureWantedForEmu(); + // Traditional Stylus Mode keeps the panel's DS touch lifecycle and + // never owns relative input. Its optional direct-aim sub-mode requests + // capture only for the held touch action, so Raw ownership follows the + // same request/active split as normal mouse aim. + const bool captureEligible = InputProjection::ShouldOwnRelativeAimInput( + focused, + m_threadBridge.PanelAvailableForEmu(), + isCursorMode, + isStylusMode, + m_enableStylusDirectAimWhileTouching, + m_threadBridge.CaptureWantedForEmu()); const bool wasInputOwner = m_inputSubscription.activeOwner.load(std::memory_order_acquire); const uint64_t wasInputGeneration = m_inputSubscription.generation; @@ -201,7 +208,7 @@ namespace MelonPrime { // This keeps instances isolated and avoids duplicate press edges when // active ownership changes. const uint64_t hotDownMask = isInputOwner - ? ((rawActionReady ? hk.down : 0) + ? ((rawActionReady ? hk.down : emuInstance->hotkeyMask) | emuInstance->joyHotkeyMask | wheelHotkeyBits) : emuInstance->hotkeyMask; if constexpr (!kReentrant) { @@ -226,13 +233,16 @@ namespace MelonPrime { #endif const InputProjection::ProjectedDownState downState = - InputProjection::ProjectDownState(hotDownMask); + InputProjection::ProjectDownState(hotDownMask, isStylusMode); m_input.down = downState.mask; m_input.moveIndex = downState.moveIndex; - // Track the V-default ScanShoot key separately from the merged IB_SHOOT bit - // so the Adventure map/user-action pause can drop the Mouse-Left ShootScan - // contribution while keeping this one. - m_scanShootKeyDown = ((hotDownMask >> HK_MetroidScanShoot) & 1ULL) != 0; + // Track the active mode-specific ScanShoot key separately from the + // merged IB_SHOOT bit so the Adventure map/user-action pause can drop + // the Mouse-Left ShootScan contribution while keeping this one. + m_scanShootKeyDown = + (downState.modeFlags & InputProjection::PMF_SCAN_SHOOT_DOWN) != 0; + m_stylusTouchKeyDown = + (downState.modeFlags & InputProjection::PMF_STYLUS_TOUCH_DOWN) != 0; #if !defined(_WIN32) bool haveMouseDelta = false; @@ -539,10 +549,19 @@ namespace MelonPrime { { if (LIKELY(emuInstance->isTouching)) { nds->TouchScreen(emuInstance->touchX, emuInstance->touchY); + return; } - else { - nds->ReleaseScreen(); + + int touchX = 0; + int touchY = 0; + if (m_stylusTouchKeyDown + && m_threadBridge.ReadStylusPointerForEmu(touchX, touchY)) + { + nds->TouchScreen(touchX, touchY); + return; } + + nds->ReleaseScreen(); } // P-29b: Cold path for aim reset (aimBlock or layout change). diff --git a/src/frontend/qt_sdl/MelonPrimeInGame.cpp b/src/frontend/qt_sdl/MelonPrimeInGame.cpp index 6bcd512df3..c6a7e6b02b 100644 --- a/src/frontend/qt_sdl/MelonPrimeInGame.cpp +++ b/src/frontend/qt_sdl/MelonPrimeInGame.cpp @@ -29,7 +29,8 @@ namespace MelonPrime { const bool isStylusMode = this->isStylusMode; // Early prefetch of aim pointers - gives ~50-100 instructions of // lead time before ProcessAimInputMouse reads them, hiding potential L2 miss. - if (LIKELY(!isStylusMode)) { + if (LIKELY(!isStylusMode) + || m_enableStylusDirectAimWhileTouching) { PREFETCH_WRITE(m_ptrs.aimX); PREFETCH_WRITE(m_ptrs.aimY); } @@ -59,9 +60,10 @@ namespace MelonPrime { m_flags.assign(StateFlags::BIT_PAUSED, isPaused); // During the map / user-action pause, the Mouse-Left ShootScan key must - // not fire (a left click stays touch-only there). Only the V-default - // ScanShoot key triggers shoot/scan/map-expand. IB_SHOOT is the OR of - // both keys in ProjectDownState, so rebuild it from the V key alone. + // not fire (a left click stays touch-only there). Only the active + // mode-specific ScanShoot key triggers shoot/scan/map-expand. + // IB_SHOOT is the OR of both actions in ProjectDownState, so rebuild + // it from that selected key alone. if (isPaused) { m_input.down = (m_input.down & ~IB_SHOOT) | (m_scanShootKeyDown ? IB_SHOOT : 0ULL); @@ -114,7 +116,27 @@ namespace MelonPrime { if (isStylusMode) { if (!m_flags.test(StateFlags::BIT_BLOCK_STYLUS)) { - ProcessAimInputStylus(nds); + if (m_enableStylusDirectAimWhileTouching + && m_stylusTouchKeyDown) + { +#ifdef _WIN32 + if (m_rawFilter && m_didFrameAdvanceSinceSnapshot) + m_rawFilter->LateLatchMouseDelta( + m_rawInputSubscription, + m_input.mouseX, + m_input.mouseY); +#endif + ProcessAimInputMouse(); + if (!m_flags.test(StateFlags::BIT_LAST_FOCUSED) + || !m_aimBlockBits) + { + using namespace Consts::UI; + nds->TouchScreen(CENTER_RESET.x(), CENTER_RESET.y()); + } + } + else { + ProcessAimInputStylus(nds); + } } } else { diff --git a/src/frontend/qt_sdl/MelonPrimeInputProjection.h b/src/frontend/qt_sdl/MelonPrimeInputProjection.h index 6baffefc73..34f26a1cec 100644 --- a/src/frontend/qt_sdl/MelonPrimeInputProjection.h +++ b/src/frontend/qt_sdl/MelonPrimeInputProjection.h @@ -42,24 +42,99 @@ static_assert(HK_MetroidWeapon6 == HK_MetroidWeaponBeam + 10, "Weapon gr struct ProjectedDownState { uint64_t mask; uint32_t moveIndex; + // Reuse the structure's existing 4-byte tail padding so the hot-path + // result remains 16 bytes while carrying mode-selected actions forward. + uint32_t modeFlags; }; -[[nodiscard]] FORCE_INLINE ProjectedDownState ProjectDownState(uint64_t hotMask) noexcept +enum ProjectedModeFlag : uint32_t { + PMF_SCAN_SHOOT_DOWN = 1u << 0, + PMF_STYLUS_TOUCH_DOWN = 1u << 1, +}; + +static_assert(sizeof(ProjectedDownState) == 16, + "ProjectedDownState must remain a compact register-friendly result"); + +[[nodiscard]] constexpr bool IsActiveScanShootDown( + uint64_t hotMask, + bool stylusMode) noexcept +{ + const int activeHotkey = stylusMode + ? HK_MetroidScanShootStylus + : HK_MetroidScanShoot; + return ((hotMask >> activeHotkey) & 1ULL) != 0; +} + +static_assert(IsActiveScanShootDown(1ULL << HK_MetroidScanShoot, false)); +static_assert(!IsActiveScanShootDown(1ULL << HK_MetroidScanShoot, true)); +static_assert(!IsActiveScanShootDown(1ULL << HK_MetroidScanShootStylus, false)); +static_assert(IsActiveScanShootDown(1ULL << HK_MetroidScanShootStylus, true)); + +[[nodiscard]] constexpr bool IsNormalShootScanDown( + uint64_t hotMask, + bool stylusMode) noexcept +{ + return !stylusMode + && ((hotMask >> HK_MetroidShootScan) & 1ULL) != 0; +} + +[[nodiscard]] constexpr bool IsStylusTouchDown( + uint64_t hotMask, + bool stylusMode) noexcept +{ + return stylusMode + && ((hotMask >> HK_MetroidStylusTouch) & 1ULL) != 0; +} + +static_assert(IsNormalShootScanDown(1ULL << HK_MetroidShootScan, false)); +static_assert(!IsNormalShootScanDown(1ULL << HK_MetroidShootScan, true)); +static_assert(!IsStylusTouchDown(1ULL << HK_MetroidStylusTouch, false)); +static_assert(IsStylusTouchDown(1ULL << HK_MetroidStylusTouch, true)); + +[[nodiscard]] constexpr bool ShouldOwnRelativeAimInput( + bool focused, + bool panelAvailable, + bool cursorMode, + bool stylusMode, + bool stylusDirectAimWhileTouching, + bool captureWanted) noexcept +{ + return focused && panelAvailable && !cursorMode + && (!stylusMode || stylusDirectAimWhileTouching) + && captureWanted; +} + +static_assert(ShouldOwnRelativeAimInput( + true, true, false, false, false, true)); +static_assert(!ShouldOwnRelativeAimInput( + true, true, false, true, false, true)); +static_assert(ShouldOwnRelativeAimInput( + true, true, false, true, true, true)); + +[[nodiscard]] FORCE_INLINE ProjectedDownState ProjectDownState( + uint64_t hotMask, + bool stylusMode) noexcept { const uint32_t moveBits = static_cast((hotMask >> HK_MetroidMoveForward) & 0xFULL); + const bool scanShootDown = IsActiveScanShootDown(hotMask, stylusMode); + const bool normalShootScanDown = IsNormalShootScanDown(hotMask, stylusMode); + const bool stylusTouchDown = IsStylusTouchDown(hotMask, stylusMode); uint64_t down = static_cast(moveBits) << 6; down |= ((hotMask >> HK_MetroidJump) & 1ULL) << 0; - down |= ((((hotMask >> HK_MetroidShootScan) | - (hotMask >> HK_MetroidScanShoot)) & 1ULL) << 1); + down |= ((static_cast(normalShootScanDown) | + static_cast(scanShootDown)) << 1); down |= ((hotMask >> HK_MetroidZoom) & 1ULL) << 2; down |= ((hotMask >> HK_MetroidHoldMorphBallBoost) & 1ULL) << 4; down |= ((hotMask >> HK_MetroidWeaponCheck) & 1ULL) << 5; down |= ((hotMask >> HK_MetroidMenu) & 1ULL) << 10; - return { down, moveBits }; + const uint32_t modeFlags = + (scanShootDown ? PMF_SCAN_SHOOT_DOWN : 0u) + | (stylusTouchDown ? PMF_STYLUS_TOUCH_DOWN : 0u); + return { down, moveBits, modeFlags }; } [[nodiscard]] FORCE_INLINE uint64_t ProjectPressMask(uint64_t hotMask) noexcept diff --git a/src/frontend/qt_sdl/MelonPrimeLifecycle.cpp b/src/frontend/qt_sdl/MelonPrimeLifecycle.cpp index d128a055e8..1b37751943 100644 --- a/src/frontend/qt_sdl/MelonPrimeLifecycle.cpp +++ b/src/frontend/qt_sdl/MelonPrimeLifecycle.cpp @@ -58,6 +58,8 @@ namespace MelonPrime { m_flags.assign(StateFlags::BIT_STYLUS_MODE, s.stylusMode); isStylusMode = m_flags.test(StateFlags::BIT_STYLUS_MODE); m_snapTapMode = m_flags.test(StateFlags::BIT_SNAP_TAP); + m_enableStylusDirectAimWhileTouching = + s.stylusDirectAimWhileTouching; m_disableMphAimSmoothing = s.disableMphAimSmoothing; m_enableAimAccumulator = s.aimAccumulator; @@ -133,7 +135,9 @@ namespace MelonPrime { m_arm9HookActivationPlan.nativeAimHookMode = s.disableMphAimSmoothing ? s.nativeAimHookMode : 0; m_arm9HookActivationPlan.lowLatencyAimMode = - s.stylusMode ? LowLatencyAimMode::Off : s.lowLatencyAimMode; + (s.stylusMode && !s.stylusDirectAimWhileTouching) + ? LowLatencyAimMode::Off + : s.lowLatencyAimMode; m_arm9HookActivationPlan.immediateInputEdgeOverlay = s.immediateInputEdgeOverlay; m_arm9HookActivationPlan.nativeZoomToggle = s.nativeZoomToggle; diff --git a/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsHotkeys.inc b/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsHotkeys.inc index 42af140b80..e0f437b43d 100644 --- a/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsHotkeys.inc +++ b/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsHotkeys.inc @@ -647,6 +647,87 @@ {MenuLangId::Hebrew, "[Metroid] (עכבר שמאלי) ירי/סריקה"}, } }, + { + "[Metroid] (Mouse Left) Touch (Aim), Double-Tap Jump (Stylus Mode)", + { + {MenuLangId::Japanese, "[Metroid] (マウス左) タッチ(エイム)、ダブルタッチジャンプ(タッチペン操作モード)"}, + {MenuLangId::German, "[Metroid] (Linke Maustaste) Berühren (Zielen), Doppeltippen zum Springen (Touchpen-Modus)"}, + {MenuLangId::Spanish, "[Metroid] (Clic izquierdo) Tocar (apuntar), doble toque para saltar (modo de lápiz táctil)"}, + {MenuLangId::French, "[Metroid] (Clic gauche) Toucher (viser), double toucher pour sauter (mode stylet)"}, + {MenuLangId::Italian, "[Metroid] (Tasto sinistro) Tocca (mira), doppio tocco per saltare (modalità pennino)"}, + {MenuLangId::Dutch, "[Metroid] (Linkermuisknop) Aanraken (richten), dubbeltikken om te springen (stylusmodus)"}, + {MenuLangId::Portuguese, "[Metroid] (Botão esquerdo) Tocar (mirar), toque duplo para saltar (modo de caneta)"}, + {MenuLangId::Russian, "[Metroid] (ЛКМ) Касание (прицеливание), двойное касание для прыжка (режим стилуса)"}, + {MenuLangId::ChineseSimplified, "[Metroid] (鼠标左键) 触摸(瞄准)、双击触摸跳跃(触控笔模式)"}, + {MenuLangId::ChineseTraditional, "[Metroid] (滑鼠左鍵) 觸控(瞄準)、雙觸跳躍(觸控筆模式)"}, + {MenuLangId::ChineseHongKong, "[Metroid] (滑鼠左鍵) 觸控(瞄準)、雙觸跳躍(觸控筆模式)"}, + {MenuLangId::Korean, "[Metroid] (마우스 왼쪽) 터치(조준), 더블 터치 점프(스타일러스 모드)"}, + {MenuLangId::Arabic, "[Metroid] (زر الفأرة الأيسر) لمس (تصويب)، لمس مزدوج للقفز (وضع القلم)"}, + {MenuLangId::Indonesian, "[Metroid] (Klik kiri) Sentuh (bidik), sentuh ganda untuk melompat (mode stylus)"}, + {MenuLangId::Ukrainian, "[Metroid] (Ліва кнопка миші) Дотик (прицілювання), подвійний дотик для стрибка (режим стилуса)"}, + {MenuLangId::Greek, "[Metroid] (Αριστερό κλικ) Άγγιγμα (στόχευση), διπλό άγγιγμα για άλμα (λειτουργία γραφίδας)"}, + {MenuLangId::Swedish, "[Metroid] (Vänster musknapp) Tryck (sikta), dubbeltryck för hopp (pekpenneläge)"}, + {MenuLangId::Thai, "[Metroid] (คลิกซ้าย) แตะ (เล็ง), แตะสองครั้งเพื่อกระโดด (โหมดสไตลัส)"}, + {MenuLangId::Czech, "[Metroid] (Levé tlačítko myši) Dotyk (míření), dvojitý dotyk pro skok (režim stylusu)"}, + {MenuLangId::Danish, "[Metroid] (Venstre museknap) Tryk (sigt), dobbelttryk for at hoppe (stylustilstand)"}, + {MenuLangId::Turkish, "[Metroid] (Sol tık) Dokun (nişan al), zıplamak için çift dokun (dokunmatik kalem modu)"}, + {MenuLangId::Norwegian, "[Metroid] (Venstre museknapp) Trykk (sikt), dobbelttrykk for å hoppe (pekepennmodus)"}, + {MenuLangId::Hungarian, "[Metroid] (Bal egérgomb) Érintés (célzás), dupla érintés az ugráshoz (érintőtoll mód)"}, + {MenuLangId::Finnish, "[Metroid] (Vasen hiiren painike) Kosketus (tähtäys), kaksoiskosketus hyppyyn (kosketuskynätila)"}, + {MenuLangId::Vietnamese, "[Metroid] (Chuột trái) Chạm (ngắm), chạm đúp để nhảy (chế độ bút cảm ứng)"}, + {MenuLangId::Polish, "[Metroid] (Lewy przycisk myszy) Dotyk (celowanie), podwójny dotyk, aby skoczyć (tryb rysika)"}, + {MenuLangId::Romanian, "[Metroid] (Clic stânga) Atingere (țintire), atingere dublă pentru salt (mod stylus)"}, + {MenuLangId::Filipino, "[Metroid] (Kaliwang pindutan ng panturo) Pindot (pagpuntirya), dobleng pindot para tumalon (stylus mode)"}, + {MenuLangId::Swahili, "[Metroid] (Kitufe cha kushoto cha kipanya) Gusa (lenga), gusa mara mbili kuruka (hali ya kalamu)"}, + {MenuLangId::Croatian, "[Metroid] (lijeva tipka miša) Dodir (ciljanje), dvostruki dodir za skok (način pisaljke)"}, + {MenuLangId::Icelandic, "[Metroid] (vinstri músarhnappur) Snerta (miða), tvísmella til að hoppa (snertipennahamur)"}, + {MenuLangId::Hindi, "[Metroid] (माउस बायाँ) स्पर्श (निशाना), कूदने के लिए दो बार स्पर्श (स्टाइलस मोड)"}, + {MenuLangId::Bengali, "[Metroid] (মাউস বাম) স্পর্শ (লক্ষ্য), লাফ দিতে দুইবার স্পর্শ (স্টাইলাস মোড)"}, + {MenuLangId::Urdu, "[Metroid] (ماؤس بایاں) ٹچ (نشانہ)، چھلانگ کے لیے ڈبل ٹچ (اسٹائلس موڈ)"}, + {MenuLangId::Punjabi, "[Metroid] (ਮਾਊਸ ਖੱਬਾ) ਛੋਹ (ਨਿਸ਼ਾਨਾ), ਛਾਲ ਲਈ ਦੋ ਵਾਰ ਛੋਹ (ਸਟਾਈਲਸ ਮੋਡ)"}, + {MenuLangId::Telugu, "[Metroid] (మౌస్ ఎడమ) టచ్ (గురి), దూకడానికి డబుల్ టచ్ (స్టైలస్ మోడ్)"}, + {MenuLangId::Persian, "[Metroid] (ماوس چپ) لمس (نشانه‌گیری)، لمس دوگانه برای پرش (حالت قلم)"}, + {MenuLangId::Malay, "[Metroid] (tetikus kiri) Sentuh (bidik), sentuh dua kali untuk melompat (mod stilus)"}, + {MenuLangId::Marathi, "[Metroid] (माउस डावे) स्पर्श (लक्ष्य), उडीसाठी दुहेरी स्पर्श (स्टायलस मोड)"}, + {MenuLangId::Tamil, "[Metroid] (மவுஸ் இடது) தொடு (குறி), குதிக்க இருமுறை தொடு (ஸ்டைலஸ் முறை)"}, + {MenuLangId::Gujarati, "[Metroid] (માઉસ ડાબું) સ્પર્શ (નિશાન), કૂદવા માટે બે વાર સ્પર્શ (સ્ટાઇલસ મોડ)"}, + {MenuLangId::Kannada, "[Metroid] (ಮೌಸ್ ಎಡ) ಸ್ಪರ್ಶ (ಗುರಿ), ಜಿಗಿಯಲು ಎರಡು ಬಾರಿ ಸ್ಪರ್ಶ (ಸ್ಟೈಲಸ್ ಮೋಡ್)"}, + {MenuLangId::Odia, "[Metroid] (ବାମ ମାଉସ୍ ବଟନ୍) ସ୍ପର୍ଶ (ଲକ୍ଷ୍ୟ), ଡେଇଁବାକୁ ଦୁଇଥର ସ୍ପର୍ଶ (ଷ୍ଟାଇଲସ୍ ମୋଡ୍)"}, + {MenuLangId::Malayalam, "[Metroid] (മൗസ് ഇടത്) ടച്ച് (ലക്ഷ്യം), ചാടാൻ ഇരട്ട ടച്ച് (സ്റ്റൈലസ് മോഡ്)"}, + {MenuLangId::Burmese, "[Metroid] (ဘယ်ဘက် မောက်စ် ခလုတ်) ထိပါ (ချိန်ရွယ်), ခုန်ရန် နှစ်ချက်ထိပါ (စတိုင်လပ်စ်မုဒ်)"}, + {MenuLangId::Uzbek, "[Metroid] (sichqoncha chap) Tegish (nishonga olish), sakrash uchun ikki marta tegish (stilus rejimi)"}, + {MenuLangId::Azerbaijani, "[Metroid] (Siçanın sol düyməsi) Toxun (nişan al), tullanmaq üçün iki dəfə toxun (stilus rejimi)"}, + {MenuLangId::Nepali, "[Metroid] (बायाँ माउस बटन) छुनुहोस् (निशाना), उफ्रन दुई पटक छुनुहोस् (स्टाइलस मोड)"}, + {MenuLangId::Sinhala, "[Metroid] (වම් මවුස් බොත්තම) ස්පර්ශය (ඉලක්කය), පැනීමට දෙවරක් ස්පර්ශ කරන්න (ස්ටයිලස් මාදිලිය)"}, + {MenuLangId::Khmer, "[Metroid] (ប៊ូតុងកណ្ដុរឆ្វេង) ប៉ះ (តម្រង់), ប៉ះពីរដងដើម្បីលោត (របៀបស្ទីឡូ)"}, + {MenuLangId::Lao, "[Metroid] (ປຸ່ມເມົາສ໌ຊ້າຍ) ແຕະ (ເລັງ), ແຕະສອງຄັ້ງເພື່ອໂດດ (ໂໝດສະໄຕລັດ)"}, + {MenuLangId::Armenian, "[Metroid] (մկնիկ ձախ) Հպում (նշանառում), կրկնակի հպում՝ ցատկելու համար (ստիլուսի ռեժիմ)"}, + {MenuLangId::Albanian, "[Metroid] (mausi majtas) Prek (shënjestro), prekje e dyfishtë për kërcim (modaliteti i stilolapsit)"}, + {MenuLangId::Georgian, "[Metroid] (მარცხენა მაუსის ღილაკი) შეხება (დამიზნება), ორმაგი შეხება ხტომისთვის (სტილუსის რეჟიმი)"}, + {MenuLangId::Bosnian, "[Metroid] (lijevi klik miša) Dodir (ciljanje), dvostruki dodir za skok (režim olovke)"}, + {MenuLangId::Serbian, "[Metroid] (леви тастер миша) Додир (циљање), двоструки додир за скок (режим оловке)"}, + {MenuLangId::Macedonian, "[Metroid] (лево копче на глувчето) Допир (нишанење), двоен допир за скок (режим на пенкало)"}, + {MenuLangId::Bulgarian, "[Metroid] (ляв бутон на мишката) Докосване (прицелване), двойно докосване за скок (режим на стилус)"}, + {MenuLangId::Belarusian, "[Metroid] (левая кнопка мышы) Дотык (прыцэльванне), падвойны дотык для скачка (рэжым стылуса)"}, + {MenuLangId::Maltese, "[Metroid] (buttuna tax-xellug tal-maws) Mess (immira), mess darbtejn biex taqbeż (modalità tal-istilus)"}, + {MenuLangId::Mongolian, "[Metroid] (хулганы зүүн товч) Хүрэх (онилох), үсрэхийн тулд давхар хүрэх (зүүт үзэгний горим)"}, + {MenuLangId::Latvian, "[Metroid] (peles kreisā poga) Pieskarties (tēmēt), dubultskāriens lēcienam (irbuļa režīms)"}, + {MenuLangId::Lithuanian, "[Metroid] (kairysis pelės mygtukas) Liesti (taikytis), dukart paliesti šuoliui (rašiklio režimas)"}, + {MenuLangId::Afrikaans, "[Metroid] (linkermuisknop) Raak (mik), dubbeltik om te spring (stylusmodus)"}, + {MenuLangId::Irish, "[Metroid] (cnaipe clé luiche) Tadhall (dírigh), tadhall dúbailte chun léim (mód stíleas)"}, + {MenuLangId::Assamese, "[Metroid] (বাওঁ মাউছ বুটাম) স্পৰ্শ (লক্ষ্য), জঁপিয়াবলৈ দুবাৰ স্পৰ্শ (ষ্টাইলাছ মোড)"}, + {MenuLangId::Amharic, "[Metroid] (የግራ መዳፊት አዝራር) ንካ (አነጣጥር), ለመዝለል ሁለቴ ንካ (የብዕር ሁነታ)"}, + {MenuLangId::Estonian, "[Metroid] (hiire vasak nupp) Puudutus (sihi), topeltpuudutus hüppamiseks (puutepliiatsi režiim)"}, + {MenuLangId::Kazakh, "[Metroid] (тінтуірдің сол жақ батырмасы) Түрту (көздеу), секіру үшін екі рет түрту (стилус режимі)"}, + {MenuLangId::Catalan, "[Metroid] (Ratolí esquerre) Toca (apunta), doble toc per saltar (mode de llapis tàctil)"}, + {MenuLangId::Kyrgyz, "[Metroid] (чычкандын сол баскычы) Тийүү (мээлөө), секирүү үчүн эки жолу тийүү (стилус режими)"}, + {MenuLangId::Slovak, "[Metroid] (ľavé tlačidlo myši) Dotyk (mierenie), dvojitý dotyk na skok (režim dotykového pera)"}, + {MenuLangId::Slovenian, "[Metroid] (levi gumb miške) Dotik (merjenje), dvojni dotik za skok (način pisala)"}, + {MenuLangId::Zulu, "[Metroid] (Inkinobho yegundane yesobunxele) Thinta (qondisa), thinta kabili ukuze ugxume (imodi yothi lokuthinta)"}, + {MenuLangId::Basque, "[Metroid] (Sagu ezkerra) Ukitu (apuntatu), ukitu bitan salto egiteko (arkatz digitalaren modua)"}, + {MenuLangId::Hebrew, "[Metroid] (עכבר שמאלי) מגע (כוונת), מגע כפול לקפיצה (מצב עט מגע)"}, + } + }, { "[Metroid] (V) Scan/Shoot, Map Zoom In", { @@ -728,6 +809,87 @@ {MenuLangId::Hebrew, "[Metroid] (V) סריקה/ירי, הגדלת מפה"}, } }, + { + "[Metroid] (V) Scan/Shoot, Map Zoom In (Stylus Mode)", + { + {MenuLangId::Japanese, "[Metroid] (V) スキャン/射撃、マップ拡大(タッチペン操作モード)"}, + {MenuLangId::German, "[Metroid] (V) Scannen/Schießen, Karte vergrößern (Touchpen-Modus)"}, + {MenuLangId::Spanish, "[Metroid] (V) Escanear/Disparar, zoom del mapa (modo de lápiz táctil)"}, + {MenuLangId::French, "[Metroid] (V) Scanner/Tirer, zoom carte (mode stylet)"}, + {MenuLangId::Italian, "[Metroid] (V) Scansiona/Spara, zoom mappa (modalità pennino)"}, + {MenuLangId::Dutch, "[Metroid] (V) Scannen/Schieten, kaartzoom (stylusmodus)"}, + {MenuLangId::Portuguese, "[Metroid] (V) Escanear/Atirar, zoom do mapa (modo de caneta)"}, + {MenuLangId::Russian, "[Metroid] (V) Сканирование/Стрельба, увеличение карты (режим стилуса)"}, + {MenuLangId::ChineseSimplified, "[Metroid] (V) 扫描/射击、地图放大(触控笔模式)"}, + {MenuLangId::ChineseTraditional, "[Metroid] (V) 掃描/射擊、地圖放大(觸控筆模式)"}, + {MenuLangId::ChineseHongKong, "[Metroid] (V) 掃描/射擊、地圖放大(觸控筆模式)"}, + {MenuLangId::Korean, "[Metroid] (V) 스캔/사격, 맵 확대 (스타일러스 모드)"}, + {MenuLangId::Arabic, "[Metroid] (V) مسح/إطلاق، تكبير الخريطة (وضع القلم)"}, + {MenuLangId::Indonesian, "[Metroid] (V) Pindai/Tembak, zoom peta (mode stylus)"}, + {MenuLangId::Ukrainian, "[Metroid] (V) Скан/Стріляти, збільшення карти (режим стилуса)"}, + {MenuLangId::Greek, "[Metroid] (V) Σάρωση/Πυροβολισμός, ζουμ χάρτη (λειτουργία γραφίδας)"}, + {MenuLangId::Swedish, "[Metroid] (V) Skanna/Skjut, kartzoom in (pekpenneläge)"}, + {MenuLangId::Thai, "[Metroid] (V) สแกน/ยิง, ซูมแผนที่ (โหมดสไตลัส)"}, + {MenuLangId::Czech, "[Metroid] (V) Sken/Střílet, zoom mapy (režim stylusu)"}, + {MenuLangId::Danish, "[Metroid] (V) Scan/Skyd, kortzoom ind (stylustilstand)"}, + {MenuLangId::Turkish, "[Metroid] (V) Tara/Ateş et, harita yakınlaştır (dokunmatik kalem modu)"}, + {MenuLangId::Norwegian, "[Metroid] (V) Skann/Skyt, kartzoom inn (pekepennmodus)"}, + {MenuLangId::Hungarian, "[Metroid] (V) Szkennelés/Lövés, térkép nagyítás (érintőtoll mód)"}, + {MenuLangId::Finnish, "[Metroid] (V) Skannaa/Ammu, kartan zoomaus (kosketuskynätila)"}, + {MenuLangId::Vietnamese, "[Metroid] (V) Quét/Bắn, phóng to bản đồ (chế độ bút cảm ứng)"}, + {MenuLangId::Polish, "[Metroid] (V) Skanuj/Strzelaj, powiększ mapę (tryb rysika)"}, + {MenuLangId::Romanian, "[Metroid] (V) Scanează/Trage, zoom hartă (mod stylus)"}, + {MenuLangId::Filipino, "[Metroid] (V) Scan/Baril, Palakihin ang mapa (mode ng stylus)"}, + {MenuLangId::Swahili, "[Metroid] (V) Changanua/Piga risasi, Kuza ramani (hali ya kalamu)"}, + {MenuLangId::Croatian, "[Metroid] (V) Skeniraj/Pucaj, Povećaj mapu (način pisaljke)"}, + {MenuLangId::Icelandic, "[Metroid] (V) Skanna/Skjóta, zooma inn á kort (snertipennahamur)"}, + {MenuLangId::Hindi, "[Metroid] (V) स्कैन/शूट, मैप ज़ूम इन (स्टाइलस मोड)"}, + {MenuLangId::Bengali, "[Metroid] (V) স্ক্যান/শুট, ম্যাপ জুম ইন (স্টাইলাস মোড)"}, + {MenuLangId::Urdu, "[Metroid] (V) اسکین/شوٹ، میپ زوم اِن (اسٹائلس موڈ)"}, + {MenuLangId::Punjabi, "[Metroid] (V) ਸਕੈਨ/ਸ਼ੂਟ, ਮੈਪ ਜ਼ੂਮ ਇਨ (ਸਟਾਈਲਸ ਮੋਡ)"}, + {MenuLangId::Telugu, "[Metroid] (V) స్కాన్/షూట్, మ్యాప్ జూమ్ ఇన్ (స్టైలస్ మోడ్)"}, + {MenuLangId::Persian, "[Metroid] (V) اسکن/شلیک، زوم نقشه (حالت قلم لمسی)"}, + {MenuLangId::Malay, "[Metroid] (V) Imbas/Tembak, zum peta masuk (mod stilus)"}, + {MenuLangId::Marathi, "[Metroid] (V) स्कॅन/शूट, मॅप झूम इन (स्टायलस मोड)"}, + {MenuLangId::Tamil, "[Metroid] (V) ஸ்கேன்/சுடு, வரைபடத்தை பெரிதாக்கு (ஸ்டைலஸ் முறை)"}, + {MenuLangId::Gujarati, "[Metroid] (V) સ્કેન/ગોળી ચલાવો, નકશો મોટો કરો (સ્ટાઇલસ મોડ)"}, + {MenuLangId::Kannada, "[Metroid] (V) ಸ್ಕ್ಯಾನ್/ಗುಂಡು ಹಾರಿಸಿ, ನಕ್ಷೆ ದೊಡ್ಡದಾಗಿಸಿ (ಸ್ಟೈಲಸ್ ಮೋಡ್)"}, + {MenuLangId::Odia, "[Metroid] (V) ସ୍କାନ/ଗୁଳି, ମ୍ୟାପ୍ ବଡ଼ କରନ୍ତୁ (ଷ୍ଟାଇଲସ୍ ମୋଡ୍)"}, + {MenuLangId::Malayalam, "[Metroid] (V) സ്കാൻ/വെടിവെക്കുക, മാപ്പ് വലുതാക്കുക (സ്റ്റൈലസ് മോഡ്)"}, + {MenuLangId::Burmese, "[Metroid] (V) စကန်/ပစ်ခတ်, မြေပုံချဲ့ (စတိုင်လပ်စ်မုဒ်)"}, + {MenuLangId::Uzbek, "[Metroid] (V) Skanlash/Otish, Xaritani kattalashtirish (stilus rejimi)"}, + {MenuLangId::Azerbaijani, "[Metroid] (V) Skan/Atəş, Xəritəni böyüt (stilus rejimi)"}, + {MenuLangId::Nepali, "[Metroid] (V) स्क्यान/गोली चलाउनु, नक्सा ठूलो पार्नु (स्टाइलस मोड)"}, + {MenuLangId::Sinhala, "[Metroid] (V) ස්කෑන්/වෙඩි තබන්න, සිතියම විශාල කරන්න (ස්ටයිලස් ප්‍රකාරය)"}, + {MenuLangId::Khmer, "[Metroid] (V) ស្កេន/បាញ់, ពង្រីកផែនទី (មុខងារប៊ិចប៉ះ)"}, + {MenuLangId::Lao, "[Metroid] (V) ສະແກນ/ຍິງ, ຂະຫຍາຍແຜນທີ່ (ໂໝດປາກກາສຳຜັດ)"}, + {MenuLangId::Armenian, "[Metroid] (V) Սքանել/Կրակել, Մեծացնել քարտեզը (ստիլուսի ռեժիմ)"}, + {MenuLangId::Albanian, "[Metroid] (V) Skano/Gjuaj, Zmadho hartën (modaliteti i stilolapsit)"}, + {MenuLangId::Georgian, "[Metroid] (V) სკანირება/სროლა, რუკის გადიდება (სტილუსის რეჟიმი)"}, + {MenuLangId::Bosnian, "[Metroid] (V) Skeniraj/Pucaj, Uvećaj mapu (način olovke)"}, + {MenuLangId::Serbian, "[Metroid] (V) Скенирај/Пуцај, Увећај мапу (режим оловке)"}, + {MenuLangId::Macedonian, "[Metroid] (V) Скенирај/Пукај, Зголеми мапа (режим на пенкало)"}, + {MenuLangId::Bulgarian, "[Metroid] (V) Сканирай/Стреляй, Увеличи картата (режим със стилус)"}, + {MenuLangId::Belarusian, "[Metroid] (V) Сканаваць/Страляць, Павялічыць карту (рэжым стылуса)"}, + {MenuLangId::Maltese, "[Metroid] (V) Skennja/Spara, Kabbar il-mappa (modalità tal-istilus)"}, + {MenuLangId::Mongolian, "[Metroid] (V) Скан хийх/Буудах, Газрыг томруулах (зүүт үзгийн горим)"}, + {MenuLangId::Latvian, "[Metroid] (V) Skenēt/Šaut, Pietuvināt karti (irbuļa režīms)"}, + {MenuLangId::Lithuanian, "[Metroid] (V) Skenuoti/Šauti, Priartinti žemėlapį (rašiklio režimas)"}, + {MenuLangId::Afrikaans, "[Metroid] (V) Skandeer/Skiet, Vergroot kaart (stylusmodus)"}, + {MenuLangId::Irish, "[Metroid] (V) Scanadh/Lámhach, Zúmáil isteach ar an léarscáil (mód stíle)"}, + {MenuLangId::Assamese, "[Metroid] (V) স্কেন/গুলি, মানচিত্ৰ ডাঙৰ কৰক (ষ্টাইলাছ মোড)"}, + {MenuLangId::Amharic, "[Metroid] (V) ስካን/መተኮስ, ካርታ አጉላ (የስታይለስ ሁነታ)"}, + {MenuLangId::Estonian, "[Metroid] (V) Skanni/Tulista, Suurenda kaarti (pliiatsirežiim)"}, + {MenuLangId::Kazakh, "[Metroid] (V) Сканерлеу/Ату, Картаны үлкейту (стилус режимі)"}, + {MenuLangId::Catalan, "[Metroid] (V) Escaneja/Dispara, Amplia el mapa (mode llapis tàctil)"}, + {MenuLangId::Kyrgyz, "[Metroid] (V) Сканерлөө/Атуу, Картаны чоңойтуу (стилус режими)"}, + {MenuLangId::Slovak, "[Metroid] (V) Skenovať/Strieľať, Priblížiť mapu (režim dotykového pera)"}, + {MenuLangId::Slovenian, "[Metroid] (V) Skeniraj/Streljaj, Povečaj zemljevid (način pisala)"}, + {MenuLangId::Zulu, "[Metroid] (V) Skena/Dubula, Sondeza imephu (imodi yosiba lokuthinta)"}, + {MenuLangId::Basque, "[Metroid] (V) Eskaneatu/Tiro egin, Mapa handitu (arkatz-modua)"}, + {MenuLangId::Hebrew, "[Metroid] (V) סריקה/ירי, הגדלת מפה (מצב עט מגע)"}, + } + }, { "[Metroid] (Mouse Right) Imperialist Zoom, Map Zoom Out, Morph Ball Boost", { diff --git a/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidGameplay.inc b/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidGameplay.inc index 191253b81c..884c0ec97d 100644 --- a/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidGameplay.inc +++ b/src/frontend/qt_sdl/MelonPrimeLocalization/inc/MelonPrimeTranslationsMetroidGameplay.inc @@ -1295,6 +1295,87 @@ {MenuLangId::Hebrew, "הפעל מצב עט מגע (השאר כבוי אלא אם ברצונך לשחק בעט מגע)"}, } }, + { + "Use Normal Mouse Aim While the Stylus Touch Key Is Held (Detects cursor movement and writes aim directly instead of aiming through DS touch input)", + { + {MenuLangId::Japanese, "タッチペン操作モード時、タッチキーを押している間は通常方式のマウスエイムを使用する(DS画面タッチではなく、カーソル移動を検出してエイムを直接書き込みます)"}, + {MenuLangId::German, "Normale Mauszielsteuerung verwenden, solange die Touchpen-Taste gehalten wird (erkennt die Cursorbewegung und schreibt das Zielen direkt, statt über die DS-Toucheingabe zu zielen)"}, + {MenuLangId::Spanish, "Usar la puntería normal con ratón mientras se mantenga pulsada la tecla táctil del lápiz (detecta el movimiento del cursor y escribe la puntería directamente en vez de usar la entrada táctil de DS)"}, + {MenuLangId::French, "Utiliser la visée normale à la souris tant que la touche tactile du stylet est maintenue (détecte le mouvement du curseur et écrit directement la visée au lieu de passer par l’entrée tactile DS)"}, + {MenuLangId::Italian, "Usa la normale mira con il mouse mentre tieni premuto il tasto tocco dello stilo (rileva il movimento del cursore e scrive direttamente la mira invece di usare l’input tattile DS)"}, + {MenuLangId::Dutch, "Normaal richten met de muis zolang de aanraaktoets van de stylus wordt ingedrukt (detecteert cursorbeweging en schrijft de richting rechtstreeks in plaats van via de DS-aanraakinvoer)"}, + {MenuLangId::Portuguese, "Usar a mira normal do rato enquanto a tecla de toque da caneta estiver premida (deteta o movimento do cursor e escreve a mira diretamente em vez de usar a entrada tátil da DS)"}, + {MenuLangId::Russian, "Использовать обычное прицеливание мышью, пока удерживается клавиша касания стилуса (движение курсора определяется и прицел записывается напрямую, без сенсорного ввода DS)"}, + {MenuLangId::ChineseSimplified, "按住触控笔触摸键时使用普通鼠标瞄准(检测光标移动并直接写入瞄准,而不是通过 DS 触控输入瞄准)"}, + {MenuLangId::ChineseTraditional, "按住觸控筆觸控鍵時使用一般滑鼠瞄準(偵測游標移動並直接寫入瞄準,而不是透過 DS 觸控輸入瞄準)"}, + {MenuLangId::ChineseHongKong, "按住觸控筆觸控鍵時使用一般滑鼠瞄準(偵測游標移動並直接寫入瞄準,而不是透過 DS 觸控輸入瞄準)"}, + {MenuLangId::Korean, "터치펜 터치 키를 누르는 동안 일반 마우스 조준 사용 (DS 터치 입력을 거치지 않고 커서 움직임을 감지해 조준을 직접 기록합니다)"}, + {MenuLangId::Arabic, "استخدام التصويب العادي بالماوس أثناء الضغط على مفتاح لمس القلم (يكتشف حركة المؤشر ويكتب التصويب مباشرة بدلاً من استخدام إدخال لمس DS)"}, + {MenuLangId::Indonesian, "Gunakan bidikan mouse normal selama tombol sentuh pena ditahan (mendeteksi gerakan kursor dan menulis bidikan secara langsung, bukan melalui input sentuh DS)"}, + {MenuLangId::Ukrainian, "Використовувати звичайне прицілювання мишею, доки утримується клавіша дотику стилуса (рух курсора визначається, а приціл записується напряму без сенсорного вводу DS)"}, + {MenuLangId::Greek, "Χρήση κανονικής στόχευσης με ποντίκι όσο κρατιέται το πλήκτρο αφής της γραφίδας (ανιχνεύει την κίνηση του δείκτη και γράφει απευθείας τη στόχευση αντί για είσοδο αφής DS)"}, + {MenuLangId::Swedish, "Använd vanlig mussiktning medan pennans beröringstangent hålls nere (känner av markörens rörelse och skriver siktet direkt i stället för via DS-pekinmatning)"}, + {MenuLangId::Thai, "ใช้การเล็งด้วยเมาส์แบบปกติขณะกดปุ่มสัมผัสของปากกา (ตรวจจับการเคลื่อนที่ของเคอร์เซอร์และเขียนค่าการเล็งโดยตรงแทนการเล็งผ่านอินพุตสัมผัส DS)"}, + {MenuLangId::Czech, "Používat běžné míření myší, dokud je držena dotyková klávesa pera (zjišťuje pohyb kurzoru a zapisuje míření přímo namísto použití dotykového vstupu DS)"}, + {MenuLangId::Danish, "Brug normal musesigtning, mens pennens berøringstast holdes nede (registrerer markørbevægelse og skriver sigtet direkte i stedet for at bruge DS-berøringsinput)"}, + {MenuLangId::Turkish, "Dokunmatik kalem dokunma tuşu basılıyken normal fare nişanını kullan (DS dokunmatik girdisi yerine imleç hareketini algılar ve nişanı doğrudan yazar)"}, + {MenuLangId::Norwegian, "Bruk vanlig musesikting mens pennens berøringstast holdes inne (registrerer markørbevegelse og skriver siktingen direkte i stedet for å bruke DS-berøringsinndata)"}, + {MenuLangId::Hungarian, "Normál egércélzás használata az érintőtoll érintőgombjának nyomva tartásakor (a kurzormozgást érzékeli és közvetlenül írja a célzást a DS érintéses bevitele helyett)"}, + {MenuLangId::Finnish, "Käytä tavallista hiiritähtäystä, kun kynän kosketusnäppäintä pidetään painettuna (tunnistaa kohdistimen liikkeen ja kirjoittaa tähtäyksen suoraan DS-kosketussyötteen sijasta)"}, + {MenuLangId::Vietnamese, "Dùng ngắm chuột thông thường khi giữ phím chạm của bút cảm ứng (phát hiện chuyển động con trỏ và ghi hướng ngắm trực tiếp thay vì qua đầu vào cảm ứng DS)"}, + {MenuLangId::Polish, "Używaj zwykłego celowania myszą podczas przytrzymywania klawisza dotyku rysika (wykrywa ruch kursora i zapisuje celowanie bezpośrednio zamiast przez wejście dotykowe DS)"}, + {MenuLangId::Romanian, "Folosește țintirea normală cu mouse-ul cât timp este ținută tasta de atingere a stylusului (detectează mișcarea cursorului și scrie țintirea direct în locul intrării tactile DS)"}, + {MenuLangId::Filipino, "Gamitin ang normal na pagpuntirya ng mouse habang hawak ang stylus touch key (tinutukoy ang galaw ng cursor at direktang isinusulat ang aim sa halip na gumamit ng DS touch input)"}, + {MenuLangId::Swahili, "Tumia ulengaji wa kawaida wa kipanya wakati kitufe cha mguso cha kalamu kimeshikiliwa (hutambua mwendo wa kishale na kuandika ulengaji moja kwa moja badala ya ingizo la mguso la DS)"}, + {MenuLangId::Croatian, "Koristi uobičajeno ciljanje mišem dok je pritisnuta dodirna tipka olovke (prepoznaje pomicanje pokazivača i izravno zapisuje ciljanje umjesto dodirnog unosa DS-a)"}, + {MenuLangId::Icelandic, "Nota venjulega miðun með mús meðan snertihnappi pennans er haldið niðri (greinir hreyfingu bendils og skrifar miðun beint í stað snertiinnsláttar DS)"}, + {MenuLangId::Hindi, "स्टाइलस टच कुंजी दबाए रखते समय सामान्य माउस निशाना उपयोग करें (DS टच इनपुट से निशाना लगाने के बजाय कर्सर की गति पहचानकर निशाना सीधे लिखता है)"}, + {MenuLangId::Bengali, "স্টাইলাস টাচ কী চেপে রাখার সময় স্বাভাবিক মাউস লক্ষ্য ব্যবহার করুন (DS টাচ ইনপুটের বদলে কার্সরের নড়াচড়া শনাক্ত করে লক্ষ্য সরাসরি লিখে)"}, + {MenuLangId::Urdu, "اسٹائلس ٹچ کلید دبائے رکھنے کے دوران عام ماؤس نشانہ استعمال کریں (DS ٹچ ان پٹ کے بجائے کرسر کی حرکت پہچان کر نشانہ براہ راست لکھتا ہے)"}, + {MenuLangId::Punjabi, "ਸਟਾਈਲਸ ਟੱਚ ਕੁੰਜੀ ਦਬਾਈ ਰੱਖਣ ਵੇਲੇ ਆਮ ਮਾਊਸ ਨਿਸ਼ਾਨਾ ਵਰਤੋ (DS ਟੱਚ ਇਨਪੁੱਟ ਦੀ ਬਜਾਏ ਕਰਸਰ ਦੀ ਹਰਕਤ ਪਛਾਣ ਕੇ ਨਿਸ਼ਾਨਾ ਸਿੱਧਾ ਲਿਖਦਾ ਹੈ)"}, + {MenuLangId::Telugu, "స్టైలస్ టచ్ కీని నొక్కి ఉంచినప్పుడు సాధారణ మౌస్ గురిని ఉపయోగించు (DS టచ్ ఇన్‌పుట్‌కు బదులుగా కర్సర్ కదలికను గుర్తించి గురిని నేరుగా వ్రాస్తుంది)"}, + {MenuLangId::Persian, "هنگام نگه داشتن کلید لمس قلم، نشانه‌گیری معمولی ماوس را استفاده کن (حرکت مکان‌نما را تشخیص می‌دهد و به‌جای ورودی لمسی DS نشانه‌گیری را مستقیم می‌نویسد)"}, + {MenuLangId::Malay, "Gunakan bidikan tetikus biasa semasa kekunci sentuh stilus ditahan (mengesan pergerakan kursor dan menulis bidikan secara terus, bukan melalui input sentuh DS)"}, + {MenuLangId::Marathi, "स्टायलस टच की दाबून ठेवताना सामान्य माउस लक्ष्य वापरा (DS टच इनपुटऐवजी कर्सरची हालचाल ओळखून लक्ष्य थेट लिहिते)"}, + {MenuLangId::Tamil, "ஸ்டைலஸ் தொடு விசையை அழுத்தியிருக்கும் போது வழக்கமான மவுஸ் குறிவைப்பைப் பயன்படுத்து (DS தொடு உள்ளீட்டுக்குப் பதிலாக சுட்டி நகர்வைக் கண்டறிந்து குறிவைப்பை நேரடியாக எழுதும்)"}, + {MenuLangId::Gujarati, "સ્ટાઇલસ ટચ કી દબાવી રાખતી વખતે સામાન્ય માઉસ નિશાન વાપરો (DS ટચ ઇનપુટને બદલે કર્સરની હલચલ ઓળખીને નિશાન સીધું લખે છે)"}, + {MenuLangId::Kannada, "ಸ್ಟೈಲಸ್ ಟಚ್ ಕೀಲಿಯನ್ನು ಒತ್ತಿ ಹಿಡಿದಿರುವಾಗ ಸಾಮಾನ್ಯ ಮೌಸ್ ಗುರಿಯನ್ನು ಬಳಸಿ (DS ಟಚ್ ಇನ್‌ಪುಟ್ ಬದಲು ಕರ್ಸರ್ ಚಲನೆಯನ್ನು ಗುರುತಿಸಿ ಗುರಿಯನ್ನು ನೇರವಾಗಿ ಬರೆಯುತ್ತದೆ)"}, + {MenuLangId::Odia, "ଷ୍ଟାଇଲସ୍ ଟଚ୍ କି ଦବାଇ ରଖିଥିବା ବେଳେ ସାଧାରଣ ମାଉସ୍ ଲକ୍ଷ୍ୟ ବ୍ୟବହାର କରନ୍ତୁ (DS ଟଚ୍ ଇନପୁଟ୍ ପରିବର୍ତ୍ତେ କର୍ସର୍ ଗତି ଚିହ୍ନଟ କରି ଲକ୍ଷ୍ୟକୁ ସିଧା ଲେଖେ)"}, + {MenuLangId::Malayalam, "സ്റ്റൈലസ് ടച്ച് കീ അമർത്തിപ്പിടിക്കുമ്പോൾ സാധാരണ മൗസ് ലക്ഷ്യമിടൽ ഉപയോഗിക്കുക (DS ടച്ച് ഇൻപുട്ടിനു പകരം കഴ്സർ ചലനം കണ്ടെത്തി ലക്ഷ്യം നേരിട്ട് എഴുതുന്നു)"}, + {MenuLangId::Burmese, "စတိုင်လပ်စ် ထိတွေ့ခလုတ်ကို ဖိထားချိန်တွင် ပုံမှန် မောက်စ်ချိန်ရွယ်မှုကို သုံးပါ (DS ထိတွေ့ထည့်သွင်းမှုအစား ကာဆာလှုပ်ရှားမှုကို သိရှိပြီး ချိန်ရွယ်မှုကို တိုက်ရိုက်ရေးသည်)"}, + {MenuLangId::Uzbek, "Stilus tegish tugmasi bosib turilganda odatiy sichqoncha nishonidan foydalanish (DS sensorli kiritishi o‘rniga kursor harakatini aniqlab, nishonni bevosita yozadi)"}, + {MenuLangId::Azerbaijani, "Stilus toxunma düyməsi basılı saxlanarkən adi siçan nişanlamasından istifadə et (DS toxunma girişi əvəzinə kursor hərəkətini aşkarlayıb nişanı birbaşa yazır)"}, + {MenuLangId::Nepali, "स्टाइलस टच कुञ्जी थिचिरहँदा सामान्य माउस लक्ष्य प्रयोग गर्नुहोस् (DS टच इनपुटको सट्टा कर्सरको चाल पत्ता लगाएर लक्ष्य सिधै लेख्छ)"}, + {MenuLangId::Sinhala, "ස්ටයිලස් ස්පර්ශ යතුර ඔබාගෙන සිටින විට සාමාන්‍ය මවුස් ඉලක්කය භාවිත කරන්න (DS ස්පර්ශ ආදානය වෙනුවට කර්සර චලනය හඳුනාගෙන ඉලක්කය සෘජුව ලියයි)"}, + {MenuLangId::Khmer, "ប្រើការតម្រង់ដោយកណ្ដុរធម្មតា ខណៈពេលសង្កត់គ្រាប់ចុចប៉ះប៊ិក (រកឃើញចលនាទស្សន៍ទ្រនិច ហើយសរសេរការតម្រង់ដោយផ្ទាល់ ជំនួសការបញ្ចូលប៉ះ DS)"}, + {MenuLangId::Lao, "ໃຊ້ການເລັງດ້ວຍເມົາສ໌ແບບປົກກະຕິຂະນະກົດປຸ່ມສຳຜັດຂອງປາກກາຄ້າງ (ກວດຈັບການເຄື່ອນຂອງເຄີເຊີ ແລະຂຽນການເລັງໂດຍກົງແທນອິນພຸດສຳຜັດ DS)"}, + {MenuLangId::Armenian, "Օգտագործել մկնիկի սովորական նշանառումը, քանի դեռ սեղմված է գրիչի հպման ստեղնը (հայտնաբերում է կուրսորի շարժումը և նշանառումն ուղղակի գրում՝ DS հպման մուտքի փոխարեն)"}, + {MenuLangId::Albanian, "Përdor shënjestrimin normal me miun ndërsa mbahet tasti i prekjes së stilolapsit (zbulon lëvizjen e kursorit dhe shkruan shënjestrimin drejtpërdrejt në vend të hyrjes me prekje DS)"}, + {MenuLangId::Georgian, "გამოიყენე მაუსით ჩვეულებრივი დამიზნება, სანამ სტილუსის შეხების ღილაკი დაჭერილია (აფიქსირებს კურსორის მოძრაობას და დამიზნებას პირდაპირ წერს DS-ის სენსორული შეყვანის ნაცვლად)"}, + {MenuLangId::Bosnian, "Koristi uobičajeno ciljanje mišem dok je pritisnuta dodirna tipka olovke (otkriva pomjeranje kursora i direktno zapisuje ciljanje umjesto dodirnog unosa DS-a)"}, + {MenuLangId::Serbian, "Користи уобичајено нишањење мишем док је притиснут тастер додира оловке (открива кретање курсора и директно уписује нишањење уместо додирног уноса DS-а)"}, + {MenuLangId::Macedonian, "Користи вообичаено нишанење со глувче додека е притиснато копчето за допир на пенкалото (го открива движењето на покажувачот и директно го запишува нишанењето наместо допирен влез DS)"}, + {MenuLangId::Bulgarian, "Използвай обичайно прицелване с мишката, докато клавишът за докосване със стилус е задържан (засича движението на курсора и записва прицела директно вместо чрез сензорния вход на DS)"}, + {MenuLangId::Belarusian, "Выкарыстоўваць звычайнае прыцэльванне мышшу, пакуль утрымліваецца клавіша дотыку стылуса (вызначае рух курсора і запісвае прыцэл непасрэдна замест сэнсарнага ўводу DS)"}, + {MenuLangId::Maltese, "Uża l-immirar normali bil-maws waqt li tinżamm il-buttuna tal-mess tal-pinna (jinduna bil-moviment tal-kursur u jikteb l-immirar direttament minflok permezz tal-input tal-mess DS)"}, + {MenuLangId::Mongolian, "Мэдрэгч үзгийн хүрэх товчийг дарж байх үед ердийн хулганын онилолтыг ашиглах (DS-ийн мэдрэгчийн оролтын оронд курсорын хөдөлгөөнийг илрүүлж онилолтыг шууд бичнэ)"}, + {MenuLangId::Latvian, "Izmantot parasto tēmēšanu ar peli, kamēr ir turēts irbuļa skāriena taustiņš (nosaka kursora kustību un ieraksta tēmējumu tieši, nevis izmanto DS skārienievadi)"}, + {MenuLangId::Lithuanian, "Naudoti įprastą taikymą pele, kol laikomas rašiklio lietimo klavišas (aptinka žymeklio judėjimą ir įrašo taikymą tiesiogiai, užuot naudojus DS jutiklinę įvestį)"}, + {MenuLangId::Afrikaans, "Gebruik gewone muismik terwyl die stylus-aanraakknoppie gehou word (bespeur wyserbeweging en skryf die mik direk in plaas van deur DS-raakinvoer)"}, + {MenuLangId::Irish, "Úsáid gnáthaimsiú luiche fad atá eochair tadhaill an stíle á coinneáil (braitheann gluaiseacht an chúrsóra agus scríobhann an t-aimsiú go díreach in ionad ionchur tadhaill DS)"}, + {MenuLangId::Assamese, "ষ্টাইলাছ টাচ কী ধৰি ৰখাৰ সময়ত স্বাভাৱিক মাউছ লক্ষ্য ব্যৱহাৰ কৰক (DS টাচ ইনপুটৰ সলনি কাৰ্ছৰৰ গতি ধৰা পেলাই লক্ষ্য পোনপটীয়াকৈ লিখে)"}, + {MenuLangId::Amharic, "የስታይለስ ንክኪ ቁልፍ ተጭኖ ሳለ መደበኛ የመዳፊት ኢላማ ተጠቀም (ከDS ንክኪ ግቤት ይልቅ የጠቋሚውን እንቅስቃሴ አውቆ ኢላማውን በቀጥታ ይጽፋል)"}, + {MenuLangId::Estonian, "Kasuta tavalist hiirega sihtimist, kui pliiatsi puuteklahvi hoitakse all (tuvastab kursori liikumise ja kirjutab sihi otse, mitte DS-i puutesisendi kaudu)"}, + {MenuLangId::Kazakh, "Стилустың түрту пернесі басылып тұрғанда кәдімгі тінтуірмен көздеуді пайдалану (DS сенсорлық енгізуінің орнына курсор қозғалысын анықтап, көздеуді тікелей жазады)"}, + {MenuLangId::Catalan, "Utilitza la punteria normal amb ratolí mentre es manté premuda la tecla tàctil del llapis (detecta el moviment del cursor i escriu la punteria directament en lloc de fer servir l’entrada tàctil de DS)"}, + {MenuLangId::Kyrgyz, "Стилустун тийүү баскычы басылып турганда кадимки чычкан менен мээлөөнү колдонуу (DS тийүү киргизүүсүнүн ордуна курсордун кыймылын аныктап, мээлөөнү түз жазат)"}, + {MenuLangId::Slovak, "Používať bežné mierenie myšou, kým je držané dotykové tlačidlo pera (zisťuje pohyb kurzora a zapisuje mierenie priamo namiesto dotykového vstupu DS)"}, + {MenuLangId::Slovenian, "Uporabi običajno merjenje z miško, dokler je pritisnjena tipka za dotik pisala (zazna premikanje kazalca in neposredno zapiše merjenje namesto vnosa na dotik DS)"}, + {MenuLangId::Zulu, "Sebenzisa ukuqondisa okuvamile kwegundane ngesikhathi kubanjwe ukhiye wokuthinta wepeni (ithola ukunyakaza kwenkomba bese ibhala ukuqondisa ngqo esikhundleni sokufaka ngokuthinta kwe-DS)"}, + {MenuLangId::Basque, "Erabili sagu bidezko apuntatze arrunta ukipen-arkatzaren ukipen-tekla sakatuta dagoen bitartean (kurtsorearen mugimendua hauteman eta apuntatzea zuzenean idazten du DS ukipen-sarreraren ordez)"}, + {MenuLangId::Hebrew, "השתמש בכיוון רגיל בעכבר כל עוד מקש המגע של העט מוחזק (מזהה את תנועת הסמן וכותב את הכיוון ישירות במקום להשתמש בקלט המגע של DS)"}, + } + }, { "Enable Touch Input on the Top Screen", { diff --git a/src/frontend/qt_sdl/MelonPrimePatchLowLatencyAimHook.inc b/src/frontend/qt_sdl/MelonPrimePatchLowLatencyAimHook.inc index c29d0a5745..bbf3c61910 100644 --- a/src/frontend/qt_sdl/MelonPrimePatchLowLatencyAimHook.inc +++ b/src/frontend/qt_sdl/MelonPrimePatchLowLatencyAimHook.inc @@ -321,7 +321,9 @@ { if (!nds || !regs || !nds->MainRAM) return; - if (isStylusMode + if ((isStylusMode + && !(m_enableStylusDirectAimWhileTouching + && m_stylusTouchKeyDown)) || (m_lowLatencyAimMode != LowLatencyAimMode::ImmediateSync && m_lowLatencyAimMode != LowLatencyAimMode::MoonLikeAim)) return; diff --git a/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookPostFoldWriteVersion.inc b/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookPostFoldWriteVersion.inc index e53ccd5051..a3bf8d8b6d 100644 --- a/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookPostFoldWriteVersion.inc +++ b/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookPostFoldWriteVersion.inc @@ -60,15 +60,17 @@ { // ARM9Hook_Install gates registration on m_enableNativeAimDeltaHook // (nativeAimHookMode == 2) AND m_disableMphAimSmoothing, so both - // runtime checks are dead. Stylus mode is dynamic and ARM9Hook_Install - // does not gate on it, so the isStylusMode check is kept. Single + // runtime checks are dead. Traditional Stylus Mode is still excluded; + // its direct-aim sub-mode is admitted only for the held touch action. Single // address registered per ROM, so the postFoldPc equality check is // dead too. (void)arm9ExecAddr; if (!nds || !regs) return; - if (isStylusMode) + if (isStylusMode + && !(m_enableStylusDirectAimWhileTouching + && m_stylusTouchKeyDown)) return; if ((m_aimBlockBits | static_cast(m_isLayoutChangePending)) != 0) return; diff --git a/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookRegisterInjectionVersion.inc b/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookRegisterInjectionVersion.inc index 2d94484d71..54582fce2c 100644 --- a/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookRegisterInjectionVersion.inc +++ b/src/frontend/qt_sdl/MelonPrimePatchNativeAimDeltaHookRegisterInjectionVersion.inc @@ -231,13 +231,15 @@ { // ARM9Hook_Install gates registration on m_enableNativeAimDeltaHook // (nativeAimHookMode == 1) AND m_disableMphAimSmoothing AND a valid - // romGroupIndex, so those runtime checks are dead. Stylus mode is - // dynamic but ARM9Hook_Install does not gate on it, so isStylusMode - // is still checked. localPlayerPtrAddr values come from constant tables + // romGroupIndex, so those runtime checks are dead. Traditional Stylus + // Mode still rejects direct writes; its direct-aim sub-mode admits them + // only for the held touch action. localPlayerPtrAddr values come from constant tables // (always inside main RAM), so their IsMainRamRange checks are dead. if (!nds || !regs) return; - if (isStylusMode) + if (isStylusMode + && !(m_enableStylusDirectAimWhileTouching + && m_stylusTouchKeyDown)) return; if ((m_aimBlockBits | static_cast(m_isLayoutChangePending)) != 0) return; diff --git a/src/frontend/qt_sdl/MelonPrimeRawHotkeyVkBinding.cpp b/src/frontend/qt_sdl/MelonPrimeRawHotkeyVkBinding.cpp index 8fc4b67d3b..5f23af1be9 100644 --- a/src/frontend/qt_sdl/MelonPrimeRawHotkeyVkBinding.cpp +++ b/src/frontend/qt_sdl/MelonPrimeRawHotkeyVkBinding.cpp @@ -105,7 +105,8 @@ namespace MelonPrime { // R2: Eliminated std::vector bridge — passes SmallVkList pointers directly // to setHotkeyVks(int, const UINT*, size_t). // - // Result: 0 heap allocations, 1 config table lookup (was 28 + 28 vectors). + // Result: 0 heap allocations, 1 config table lookup (was one lookup and + // one vector allocation per binding). // ========================================================================= void BindMetroidHotkeysFromConfig(RawInputWinFilter* filter, RawInputSubscription* subscription, int instance) { @@ -119,7 +120,9 @@ namespace MelonPrime { static const BindingDef kBindings[] = { // Camera / Movement / Basic Actions { "Keyboard.HK_MetroidShootScan", HK_MetroidShootScan }, + { "Keyboard.HK_MetroidStylusTouch", HK_MetroidStylusTouch }, { "Keyboard.HK_MetroidScanShoot", HK_MetroidScanShoot }, + { "Keyboard.HK_MetroidScanShootStylus", HK_MetroidScanShootStylus }, { "Keyboard.HK_MetroidZoom", HK_MetroidZoom }, { "Keyboard.HK_MetroidMoveForward", HK_MetroidMoveForward }, { "Keyboard.HK_MetroidMoveBack", HK_MetroidMoveBack }, @@ -155,7 +158,7 @@ namespace MelonPrime { { "Keyboard.HK_MetroidMenu", HK_MetroidMenu }, }; - // Single config table lookup for entire batch (was 28 separate lookups) + // Single config table lookup for the entire binding batch. auto tbl = Config::GetLocalTable(instance); for (const auto& bind : kBindings) { diff --git a/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.cpp b/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.cpp index d948ffb257..283b9365cb 100644 --- a/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.cpp +++ b/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.cpp @@ -51,6 +51,8 @@ RuntimeConfigSnapshot LoadRuntimeConfigSnapshot(Config::Table& cfg) noexcept s.joy2Key = cfg.GetBool(CfgKey::Joy2Key); s.snapTap = cfg.GetBool(CfgKey::SnapTap); s.stylusMode = cfg.GetBool(CfgKey::StylusMode); + s.stylusDirectAimWhileTouching = + cfg.GetBool(CfgKey::StylusDirectAimWhileTouching); s.disableMphAimSmoothing = cfg.GetBool(CfgKey::DisableMphAimSmoothing); s.aimAccumulator = cfg.GetBool(CfgKey::AimAccumulator); diff --git a/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.h b/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.h index 84845d0c0e..b906be50cd 100644 --- a/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.h +++ b/src/frontend/qt_sdl/MelonPrimeRuntimeConfig.h @@ -70,6 +70,9 @@ struct RuntimeConfigSnapshot { bool joy2Key = false; bool snapTap = false; bool stylusMode = false; + // While Stylus Mode is active, the configured touch action gates the + // normal relative-mouse/direct-write aim path instead of DS touch aiming. + bool stylusDirectAimWhileTouching = false; // Sub-option of the TouchScreenAimOnly battle patch: lift it for the few // frames the Standard transform taps the touch-screen Transform button. bool touchScreenAimOnlySuspendForTransform = false; diff --git a/src/frontend/qt_sdl/MelonPrimeThreadBridge.h b/src/frontend/qt_sdl/MelonPrimeThreadBridge.h index 5952879958..9290375e3a 100644 --- a/src/frontend/qt_sdl/MelonPrimeThreadBridge.h +++ b/src/frontend/qt_sdl/MelonPrimeThreadBridge.h @@ -160,6 +160,16 @@ class MelonPrimeThreadBridge { m_panelAimX.store(0, std::memory_order_relaxed); m_panelAimY.store(0, std::memory_order_release); } + void PublishStylusPointerFromGui(int x, int y, bool valid) noexcept + { + uint32_t packed = 0; + if (valid) { + packed = (1u << 31) + | (static_cast(y) & 0xFFu) << 8 + | (static_cast(x) & 0xFFu); + } + m_stylusPointer.store(packed, std::memory_order_release); + } // MELONPRIME_PHASE5_CONFIG_USAGE_V1 // Packed publication keeps value and generation in one atomic operation. @@ -246,6 +256,15 @@ class MelonPrimeThreadBridge { y = m_centerY.load(std::memory_order_acquire); x = m_centerX.load(std::memory_order_relaxed); } + [[nodiscard]] bool ReadStylusPointerForEmu(int& x, int& y) const noexcept + { + const uint32_t packed = m_stylusPointer.load(std::memory_order_acquire); + if ((packed & (1u << 31)) == 0) + return false; + x = static_cast(packed & 0xFFu); + y = static_cast((packed >> 8) & 0xFFu); + return true; + } void PublishRuntimeFromEmu(bool cursorMode, bool stylusMode, bool inGame, bool romDetected, @@ -341,6 +360,9 @@ class MelonPrimeThreadBridge { std::atomic m_cursorModeCommand{-1}; std::atomic m_panelAimX{0}; std::atomic m_panelAimY{0}; + // GUI-published DS coordinate under the pointer. Packed so the emulation + // thread cannot observe X/Y from different mouse events; bit 31 is valid. + std::atomic m_stylusPointer{0}; std::atomic m_runtimeBits{1u}; std::atomic_bool m_cursorVisibleDesired{true}; std::atomic m_guiRequests{0}; diff --git a/src/frontend/qt_sdl/Screen.cpp b/src/frontend/qt_sdl/Screen.cpp index 93da74ba47..620848f79d 100755 --- a/src/frontend/qt_sdl/Screen.cpp +++ b/src/frontend/qt_sdl/Screen.cpp @@ -734,11 +734,16 @@ void ScreenPanel::loadMelonPrimeStylusCursorConfig() { auto& cfg = emuInstance->getLocalConfig(); const bool stylusModeEnabled = cfg.GetBool(MelonPrime::CfgKey::StylusMode); + stylusDirectAimWhileTouchingEnabled = stylusModeEnabled + && cfg.GetBool(MelonPrime::CfgKey::StylusDirectAimWhileTouching); stylusHideCursorInGameEnabled = stylusModeEnabled && cfg.GetBool(MelonPrime::CfgKey::StylusHideCursorInGame); stylusConfineCursorToTopScreenEnabled = stylusModeEnabled && cfg.GetBool(MelonPrime::CfgKey::StylusConfineCursorToTopScreen); + // Direct aim owns a 1px relative-input capture only while its touch action + // is held. The traditional idle-center policy must not act between presses. stylusHoldCursorAtCenterEnabled = stylusModeEnabled + && !stylusDirectAimWhileTouchingEnabled && cfg.GetBool(MelonPrime::CfgKey::StylusHoldCursorAtCenterWhenNotClicking); stylusMatchCursorOptionsEnabled = stylusHideCursorInGameEnabled || stylusConfineCursorToTopScreenEnabled @@ -749,6 +754,13 @@ void ScreenPanel::refreshStylusCursorSettings() { loadMelonPrimeStylusCursorConfig(); + if (!stylusDirectAimWhileTouchingEnabled + && m_stylusDirectAimCaptureHeld) + { + m_stylusDirectAimMouseButton = Qt::NoButton; + endStylusDirectAimCapture(); + } + auto* const core = melonPrimeCore(); const bool hasState = (core != nullptr); const auto ui = hasState ? core->ThreadBridge().ReadForGui() @@ -761,6 +773,69 @@ void ScreenPanel::refreshStylusCursorSettings() if (m_stylusMatchCursorActive) holdStylusCursorAtCenterIfNotClicking(mapFromGlobal(QCursor::pos())); } + +void ScreenPanel::primeStylusTouchHotkeyAtCursor(int qtKey) +{ + auto* const emu = emuInstance; + auto* const core = melonPrimeCore(); + if (!emu || !core + || !emu->hotkeyUsesKeyboardKey(HK_MetroidStylusTouch, qtKey)) + { + return; + } + + const auto ui = core->ThreadBridge().ReadForGui(); + if (!ui.stylusMode || !ui.inGame) + return; + + if (stylusDirectAimWhileTouchingEnabled && !ui.cursorMode) { + beginStylusDirectAimCapture(); + return; + } + + QPoint local = mapFromGlobal(QCursor::pos()); + int x = local.x(); + int y = local.y(); + const bool valid = getTouchCoords(x, y, false); + core->ThreadBridge().PublishStylusPointerFromGui(x, y, valid); + // Merely sampling the hover coordinate must not claim a top-screen drag. + topScreenTouchTransform = -1; +} + +void ScreenPanel::releaseStylusTouchHotkeyCapture(int qtKey) +{ + auto* const emu = emuInstance; + if (!emu || !m_stylusDirectAimCaptureHeld + || !emu->hotkeyUsesKeyboardKey(HK_MetroidStylusTouch, qtKey)) + { + return; + } + + endStylusDirectAimCapture(); +} + +void ScreenPanel::beginStylusDirectAimCapture() +{ + if (m_stylusDirectAimCaptureHeld) + return; + + m_stylusDirectAimCaptureHeld = true; + m_stylusClickHeld = true; + clipCursorCenter1px(); +} + +void ScreenPanel::endStylusDirectAimCapture() +{ + if (!m_stylusDirectAimCaptureHeld) + return; + + m_stylusDirectAimCaptureHeld = false; + m_stylusClickHeld = false; + unclip(); + // Restore any Stylus Mode hide/confine/not-clicking policy that was + // temporarily superseded by relative aim capture. + updateClipIfNeeded(); +} #endif bool ScreenPanel::getTouchCoords(int& x, int& y, bool clamp) @@ -1004,13 +1079,26 @@ void ScreenPanel::mousePressEvent(QMouseEvent* event) emu->onMousePress(event); #endif - if (event->button() != Qt::LeftButton) - return; - #ifdef MELONPRIME_DS // Mouse aim mode logic const auto ui = core ? core->ThreadBridge().ReadForGui() : MelonPrime::MelonPrimeUiSnapshot{}; + // During a Stylus Mode match the configured touch-contact binding owns + // mouse touch activation. Outside that narrow case, retain the normal DS + // UI behavior where the physical left button touches the screen. + if (core && ui.stylusMode && ui.inGame) { + if (!emu->hotkeyUsesMouseButton(HK_MetroidStylusTouch, event->button())) + return; + if (stylusDirectAimWhileTouchingEnabled && !ui.cursorMode) { + m_stylusDirectAimMouseButton = event->button(); + beginStylusDirectAimCapture(); + return; + } + } + else if (event->button() != Qt::LeftButton) { + return; + } + if (core && !ui.stylusMode && ui.inGame) { // If not in cursor mode (aim mode), treat click as returning to aim (clip) @@ -1021,6 +1109,10 @@ void ScreenPanel::mousePressEvent(QMouseEvent* event) } // If isCursorMode == true, proceed to standard touch processing } + m_touchMouseButton = event->button(); +#else + if (event->button() != Qt::LeftButton) + return; #endif const QPoint p = event->pos(); @@ -1031,7 +1123,16 @@ void ScreenPanel::mousePressEvent(QMouseEvent* event) { touching = true; emu->touchScreen(x, y); +#ifdef MELONPRIME_DS + if (core && ui.stylusMode && ui.inGame) + core->ThreadBridge().PublishStylusPointerFromGui(x, y, true); +#endif } +#ifdef MELONPRIME_DS + else if (core && ui.stylusMode && ui.inGame) { + core->ThreadBridge().PublishStylusPointerFromGui(0, 0, false); + } +#endif #ifdef MELONPRIME_DS // If not in cursor mode, re-clip @@ -1060,6 +1161,14 @@ void ScreenPanel::mouseReleaseEvent(QMouseEvent* event) #ifdef MELONPRIME_DS emu->onMouseRelease(event); + + if (m_stylusDirectAimMouseButton != Qt::NoButton + && event->button() == m_stylusDirectAimMouseButton) + { + m_stylusDirectAimMouseButton = Qt::NoButton; + endStylusDirectAimCapture(); + return; + } #endif if (Q_UNLIKELY(!emu->emuIsActive())) @@ -1073,8 +1182,17 @@ void ScreenPanel::mouseReleaseEvent(QMouseEvent* event) #include "MelonPrimeHudScreenCppMouseRelease.inc" +#ifdef MELONPRIME_DS + const bool releasesAcceptedTouch = (m_touchMouseButton != Qt::NoButton) + ? event->button() == m_touchMouseButton + : event->button() == Qt::LeftButton; + if (!releasesAcceptedTouch) + return; + m_touchMouseButton = Qt::NoButton; +#else if (event->button() != Qt::LeftButton) return; +#endif #ifdef MELONPRIME_DS // Ahead of the `touching` early-out below: a press whose touch never @@ -1117,19 +1235,28 @@ void ScreenPanel::mouseMoveEvent(QMouseEvent* event) #include "MelonPrimeHudScreenCppMouseMove.inc" -#if defined(MELONPRIME_DS) && (defined(__linux__) || defined(__APPLE__)) +#ifdef MELONPRIME_DS + // One immutable runtime snapshot per Qt mouse event. Reuse it for both the + // platform aim route and Stylus Mode pointer publication below. auto* const thread = emu->getEmuThread(); auto* const core = thread ? thread->GetMelonPrimeCore() : nullptr; const auto ui = core ? core->ThreadBridge().ReadForGui() : MelonPrime::MelonPrimeUiSnapshot{}; +#endif + +#if defined(MELONPRIME_DS) && (defined(__linux__) || defined(__APPLE__)) // MELONPRIME_INPUT_DEBUG=1: 1 Hz gate/event diagnostics for the Qt aim path. static const bool s_aimDbg = getenv("MELONPRIME_INPUT_DEBUG") != nullptr; if (Q_UNLIKELY(s_aimDbg)) { static int s_events = 0, s_blocked = 0; static qint64 s_lastLog = 0; - const bool gateOk = core + const bool relativeAimMode = core + && (!ui.stylusMode + || (stylusDirectAimWhileTouchingEnabled + && ui.captureWanted)); + const bool gateOk = relativeAimMode && ui.focused - && !ui.stylusMode && !ui.cursorMode && ui.inGame; + && !ui.cursorMode && ui.inGame; gateOk ? ++s_events : ++s_blocked; const qint64 now = QDateTime::currentMSecsSinceEpoch(); if (now - s_lastLog >= 1000) { @@ -1149,7 +1276,9 @@ void ScreenPanel::mouseMoveEvent(QMouseEvent* event) } if (core && ui.focused - && !ui.stylusMode + && (!ui.stylusMode + || (stylusDirectAimWhileTouchingEnabled + && ui.captureWanted)) && !ui.cursorMode && ui.inGame) { @@ -1240,6 +1369,22 @@ void ScreenPanel::mouseMoveEvent(QMouseEvent* event) #ifdef MELONPRIME_DS holdStylusCursorAtCenterIfNotClicking(event->pos()); + + // Keep a coherent DS coordinate available even when the configured touch + // action is a keyboard or joystick button rather than a physical click. + if (!touching && core && ui.stylusMode && ui.inGame + && !(stylusDirectAimWhileTouchingEnabled && ui.captureWanted)) + { + const QPoint p = event->pos(); + int x = p.x(); + int y = p.y(); + const bool valid = getTouchCoords(x, y, false); + core->ThreadBridge().PublishStylusPointerFromGui(x, y, valid); + // Hover tracking must not claim a drag transform before a contact + // actually starts. + topScreenTouchTransform = -1; + return; + } #endif if (!touching) @@ -1252,6 +1397,10 @@ void ScreenPanel::mouseMoveEvent(QMouseEvent* event) if (getTouchCoords(x, y, true)) { emu->touchScreen(x, y); +#ifdef MELONPRIME_DS + if (core && ui.stylusMode && ui.inGame) + core->ThreadBridge().PublishStylusPointerFromGui(x, y, true); +#endif } } @@ -3690,6 +3839,11 @@ void ScreenPanel::unfocus() if (core) core->ThreadBridge().SetFocusedFromGui(false); + if (m_stylusDirectAimCaptureHeld) { + m_stylusDirectAimMouseButton = Qt::NoButton; + endStylusDirectAimCapture(); + } + #if defined(MELONPRIME_DS) && defined(__APPLE__) if (emu) { emu->syncMouseHotkeysFromQtButtons(QGuiApplication::mouseButtons()); @@ -3704,8 +3858,9 @@ void ScreenPanel::unfocus() } #endif - // Focus loss is temporary. Preserve clipWanted and only release the - // active platform capture; focus/click activation can reacquire it. + // Normal mouse aim preserves clipWanted across temporary focus loss. + // A held-only Stylus direct-aim capture was explicitly cleared above so + // focus return cannot reacquire it without a fresh touch-action press. MelonPrime::ScreenCursorPolicy::Suspend(*this); } diff --git a/src/frontend/qt_sdl/Screen.h b/src/frontend/qt_sdl/Screen.h index 8ef7959bd3..360d2b4022 100755 --- a/src/frontend/qt_sdl/Screen.h +++ b/src/frontend/qt_sdl/Screen.h @@ -314,6 +314,11 @@ class ScreenPanel : public QWidget void reloadNoRomSplashLocalization(); void refreshTopScreenTouchSetting(); void refreshStylusCursorSettings(); + // Event-driven keyboard equivalent of starting a panel touch. Traditional + // Stylus Mode publishes the pointer's DS coordinate; direct-aim mode starts + // relative capture instead. The matching release always tears it down. + void primeStylusTouchHotkeyAtCursor(int qtKey); + void releaseStylusTouchHotkeyCapture(int qtKey); void containAimCursorIfNeeded(); void syncMelonPrimeThreadBridge(); // Explicit settings-dialog save wins over any older debounced hotkey save. @@ -397,6 +402,7 @@ private slots: bool stylusHideCursorInGameEnabled = false; bool stylusConfineCursorToTopScreenEnabled = false; bool stylusHoldCursorAtCenterEnabled = false; + bool stylusDirectAimWhileTouchingEnabled = false; // Cached OR of the match-scoped cursor options, so the per-pass reconcile // short-circuits on one predictable bool when they are all off. bool stylusMatchCursorOptionsEnabled = false; @@ -543,6 +549,8 @@ private slots: // Parks the pointer at the drag origin while no click is held, so every // drag starts centred with its full range available. void holdStylusCursorAtCenterIfNotClicking(const QPoint& localPos); + void beginStylusDirectAimCapture(); + void endStylusDirectAimCapture(); [[nodiscard]] QPoint stylusCursorCenterLocal() const; std::optional getScreenWidgetRect(int wantedScreenKind) const; std::optional getBottomScreenWidgetRect() const; @@ -560,9 +568,15 @@ private slots: bool m_hasLastClipInGameState = false; bool m_lastClipFocusedState = false; bool m_stylusMatchCursorActive = false; + // Mouse button accepted as the current touch contact. In a Stylus Mode + // match this follows HK_MetroidStylusTouch instead of being hard-wired to + // the left button. + Qt::MouseButton m_touchMouseButton = Qt::NoButton; + Qt::MouseButton m_stylusDirectAimMouseButton = Qt::NoButton; // Tracks the held click itself rather than `touching`: a press whose touch // never registers must still free the pointer, or the pin would trap it. bool m_stylusClickHeld = false; + bool m_stylusDirectAimCaptureHeld = false; bool m_hasLastClipFocusedState = false; // EmuThread requests a GUI-thread cursor/state reconciliation. The atomic // coalesces repeated per-frame requests without touching QWidget off-thread. diff --git a/src/frontend/qt_sdl/Window.cpp b/src/frontend/qt_sdl/Window.cpp index d648c1173b..2fba44b2f0 100755 --- a/src/frontend/qt_sdl/Window.cpp +++ b/src/frontend/qt_sdl/Window.cpp @@ -1537,6 +1537,15 @@ void MainWindow::keyPressEvent(QKeyEvent* event) emuInstance->onKeyPress(event); #ifdef MELONPRIME_DS + // A keyboard binding for the Stylus Mode touch action must begin at the + // current pointer just like a physical mouse click. This is event-driven, + // so it adds no polling or per-frame GUI work. + { + QMutexLocker panelLock(&screenPanelLock); + if (panel) + panel->primeStylusTouchHotkeyAtCursor(event->key()); + } + // MelonPrimeDS. for Escaping from metroid cursor lock if (event->key() == Qt::Key_Escape) { const auto widgets = QApplication::allWidgets(); @@ -1557,6 +1566,13 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) if (event->isAutoRepeat()) return; emuInstance->onKeyRelease(event); +#ifdef MELONPRIME_DS + { + QMutexLocker panelLock(&screenPanelLock); + if (panel) + panel->releaseStylusTouchHotkeyCapture(event->key()); + } +#endif } #if defined(MELONPRIME_DS) && defined(MELONPRIME_ENABLE_VULKAN)