Files
ppsspp/Core/ControlMapper.h
T
Henrik RydgårdandClaude Opus 5 50616825da Don't fire single-button mappings while a combo using them is held
If you map something to L2+R2, the mappings for L2 and R2 on their own
would fire as well. Now, while a combo mapping is fully held, the
shorter mappings that share an input with it are suppressed - longest
match wins. Releasing part of the combo brings the shorter mappings
back, for the inputs that are still held.

Adds a ControlMapper unit test covering the sequence.

Fixes #20621

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-03 12:37:31 -06:00

149 lines
5.3 KiB
C++

#pragma once
#include <cstring>
#include <atomic>
#include <functional>
#include <mutex>
#include <vector>
#include "Common/Input/InputState.h"
#include "Core/KeyMap.h"
struct DisplayLayoutConfig;
// Pure interface.
class ControlListener {
public:
virtual ~ControlListener() = default;
virtual void OnVKey(VirtKey vkey, bool down) {}
virtual void OnVKeyAnalog(VirtKey vkey, float value) {}
virtual void UpdatePSPButtons(uint32_t buttonMask, uint32_t changedMask) {}
virtual void SetPSPAnalog(int rotation, int stick, float x, float y) {}
virtual void SetRawAnalog(int stick, float x, float y) {}
};
class StringWriter;
// Utilities for mapping input events to PSP inputs and virtual keys.
// Main use is of course from EmuScreen.cpp, but also useful from control settings etc.
class ControlMapper {
public:
void UpdateConfig(const DisplayLayoutConfig &config);
void UpdateAutoMovements(double now);
// Inputs to the table-based mapping
// These functions are free-threaded.
bool Key(const KeyInput &key);
void Axis(const AxisInput *axes, size_t count);
// Required callbacks.
// TODO: These are so many now that a virtual interface might be more appropriate..
// Both of these take mutex_ - listeners_ is iterated on the input thread, and screens add and
// remove themselves from another one.
void AddListener(ControlListener *listener);
void RemoveListener(ControlListener *listener);
// Inject raw PSP key input directly, such as from touch screen controls.
// Combined with the mapped input. Unlike __Ctrl APIs, this supports
// virtual key codes, including analog mappings.
void PSPKey(int deviceId, int pspKeyCode, KeyInputFlags flags);
// Toggle swapping DPAD and Analog. Useful on some input devices with few buttons.
void ToggleSwapAxes();
// Call this when a Vkey press triggers leaving the screen you're using the controlmapper on. This can cause
// the loss of key-up events, which will confuse things later when you're back.
// Might replace this later by allowing through "key-up" and similar events to lower screens.
void ForceReleaseVKey(int vkey);
// Call when the emu screen gets pushed behind some other screen, like the pause screen, to release all "down" inputs.
void ReleaseAll();
void GetDebugString(StringWriter &w) const;
bool PollPauseTrigger() {
return pauseTrigger_.exchange(false);
}
struct InputSample {
float value;
double timestamp;
};
private:
void UpdateSwapAxes();
bool UpdatePSPState(const InputMapping &changedMapping, double now);
void UpdateComboSuppression();
bool IsSuppressedByCombo(const KeyMap::MultiInputMapping &multiMapping) const;
bool SuppressionChanged(const KeyMap::MultiInputMapping &multiMapping) const;
float MapAxisValue(float value, int vkId, const InputMapping &mapping, const InputMapping &changedMapping, bool *oppositeTouched);
void SwapMappingIfEnabled(uint32_t *vkey);
void SetPSPAxis(int deviceId, int stick, char axis, float value);
void UpdateAnalogOutput(int stick);
void onVKey(VirtKey vkey, bool down);
void onVKeyAnalog(int deviceId, VirtKey vkey, float value);
void UpdateCurInputAxis(const InputMapping &mapping, float value, double timestamp);
float GetDeviceAxisThreshold(int device, const InputMapping &mapping);
bool IsVirtKeyOn(VirtKey key) const {
int index = key - VIRTKEY_FIRST;
if (index < 0 || index >= VIRTKEY_COUNT) {
return false;
}
return virtKeyOn_[index];
}
// To track mappable virtual keys. We can have as many as we want.
float virtKeys_[VIRTKEY_COUNT]{};
bool virtKeyOn_[VIRTKEY_COUNT]{}; // Track boolean output separaately since thresholds may differ.
// This is only used for co-axis (analog stick to buttons), so not bothering to track separately
// per device.
float rawAxisValue_[JOYSTICK_AXIS_MAX]{};
double deviceTimestamps_[(size_t)DEVICE_ID_COUNT]{};
int lastNonDeadzoneDeviceID_[2]{};
float history_[2][2]{};
float converted_[2][2]{}; // for debug display
// Mappable auto-rotation. Useful for keyboard/dpad->analog in a few games.
bool autoRotatingAnalogCW_ = false;
bool autoRotatingAnalogCCW_ = false;
std::atomic<bool> pauseTrigger_{};
bool swapAxes_ = false;
int iInternalScreenRotationCached_ = 0;
// Protects basically all the state. (There is no screenmanager mutex to piggyback on, despite
// what a previous comment here claimed - input arrives on its own thread.)
std::mutex mutex_;
std::map<InputMapping, InputSample> curInput_;
// While a combo mapping is fully held, the shorter mappings that its inputs also belong to are
// suppressed - see UpdateComboSuppression. Maps an input to the size of the longest satisfied
// combo it takes part in, so a mapping is suppressed if it's shorter than that.
std::map<InputMapping, size_t> comboSuppression_;
// Every combo mapping in the keymap. Cached, since scanning them all isn't free and the
// mappings only change when the user edits them.
std::vector<KeyMap::MultiInputMapping> comboMappings_;
int comboMappingsGeneration_ = -1;
// Inputs whose suppression state changed in the current update, see UpdateComboSuppression.
std::vector<InputMapping> comboSuppressionChanged_;
// Callbacks
std::vector<ControlListener *> listeners_;
};
void ConvertAnalogStick(float x, float y, float *outX, float *outY);
float GetDeviceAxisThreshold(int device, const InputMapping &mapping);
extern ControlMapper g_controlMapper;