diff --git a/CMakeLists.txt b/CMakeLists.txt
index 21a2332eeb..81b943386e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1492,6 +1492,8 @@ add_library(${CoreLibName} ${CoreLinkType}
Core/Debugger/WebSocket/GPURecordSubscriber.h
Core/Debugger/WebSocket/HLESubscriber.cpp
Core/Debugger/WebSocket/HLESubscriber.h
+ Core/Debugger/WebSocket/InputSubscriber.cpp
+ Core/Debugger/WebSocket/InputSubscriber.h
Core/Debugger/WebSocket/LogBroadcaster.cpp
Core/Debugger/WebSocket/LogBroadcaster.h
Core/Debugger/WebSocket/MemorySubscriber.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index e91c514383..d899148602 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -437,6 +437,7 @@
+
@@ -981,6 +982,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 76fac1fe88..bb8f2bcfc5 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -965,6 +965,9 @@
Core
+
+ Debugger\WebSocket
+
@@ -1649,6 +1652,9 @@
Core
+
+ Debugger\WebSocket
+
diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp
index 9c4cdeef3c..4dad585e71 100644
--- a/Core/Debugger/WebSocket.cpp
+++ b/Core/Debugger/WebSocket.cpp
@@ -55,6 +55,7 @@
#include "Core/Debugger/WebSocket/GPUBufferSubscriber.h"
#include "Core/Debugger/WebSocket/GPURecordSubscriber.h"
#include "Core/Debugger/WebSocket/HLESubscriber.h"
+#include "Core/Debugger/WebSocket/InputSubscriber.h"
#include "Core/Debugger/WebSocket/MemorySubscriber.h"
#include "Core/Debugger/WebSocket/SteppingSubscriber.h"
@@ -67,6 +68,7 @@ static const std::vector subscribers({
&WebSocketGPUBufferInit,
&WebSocketGPURecordInit,
&WebSocketHLEInit,
+ &WebSocketInputInit,
&WebSocketMemoryInit,
&WebSocketSteppingInit,
});
diff --git a/Core/Debugger/WebSocket/InputSubscriber.cpp b/Core/Debugger/WebSocket/InputSubscriber.cpp
new file mode 100644
index 0000000000..9e6276c68c
--- /dev/null
+++ b/Core/Debugger/WebSocket/InputSubscriber.cpp
@@ -0,0 +1,256 @@
+// Copyright (c) 2021- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#include
+#include
+#include "Common/StringUtils.h"
+#include "Core/Debugger/WebSocket/InputSubscriber.h"
+#include "Core/Debugger/WebSocket/WebSocketUtils.h"
+#include "Core/HLE/sceCtrl.h"
+#include "Core/HLE/sceDisplay.h"
+
+static const std::unordered_map buttonLookup = {
+ { "cross", CTRL_CROSS },
+ { "circle", CTRL_CIRCLE },
+ { "triangle", CTRL_TRIANGLE },
+ { "square", CTRL_SQUARE },
+ { "up", CTRL_UP },
+ { "down", CTRL_DOWN },
+ { "left", CTRL_LEFT },
+ { "right", CTRL_RIGHT },
+ { "start", CTRL_START },
+ { "select", CTRL_SELECT },
+ { "home", CTRL_HOME },
+ { "screen", CTRL_SCREEN },
+ { "note", CTRL_NOTE },
+ { "ltrigger", CTRL_LTRIGGER },
+ { "rtrigger", CTRL_RTRIGGER },
+ { "hold", CTRL_HOLD },
+ { "wlan", CTRL_WLAN },
+ { "remote_hold", CTRL_REMOTE_HOLD },
+ { "vol_up", CTRL_VOL_UP },
+ { "vol_down", CTRL_VOL_DOWN },
+ { "disc", CTRL_DISC },
+ { "memstick", CTRL_MEMSTICK },
+ { "forward", CTRL_FORWARD },
+ { "back", CTRL_BACK },
+ { "playpause", CTRL_PLAYPAUSE },
+};
+
+struct WebSocketInputState : public DebuggerSubscriber {
+ void Buttons(DebuggerRequest &req);
+ void Press(DebuggerRequest &req);
+ void Analog(DebuggerRequest &req);
+
+ void Broadcast(net::WebSocketServer *ws) override;
+
+protected:
+ struct PressInfo {
+ std::string ticket;
+ uint32_t button;
+ uint32_t duration;
+
+ std::string Event();
+ };
+
+ std::vector pressTickets_;
+ int lastCounter_ = -1;
+};
+
+std::string WebSocketInputState::PressInfo::Event() {
+ JsonWriter j;
+ j.begin();
+ j.writeString("event", "input.press");
+ if (!ticket.empty()) {
+ j.writeRaw("ticket", ticket);
+ }
+ j.end();
+ return j.str();
+}
+
+DebuggerSubscriber *WebSocketInputInit(DebuggerEventHandlerMap &map) {
+ auto p = new WebSocketInputState();
+ map["input.buttons"] = std::bind(&WebSocketInputState::Buttons, p, std::placeholders::_1);
+ map["input.press"] = std::bind(&WebSocketInputState::Press, p, std::placeholders::_1);
+ map["input.analog"] = std::bind(&WebSocketInputState::Analog, p, std::placeholders::_1);
+
+ return p;
+}
+
+// Alter PSP button press flags (input.buttons)
+//
+// Parameters:
+// - buttons: object containing button names as string keys, boolean press state as value.
+//
+// Button names (some are not respected by PPSSPP):
+// - cross: button on bottom side of right pad.
+// - circle: button on right side of right pad.
+// - triangle: button on top side of right pad.
+// - square: button on left side of right pad.
+// - up: d-pad up button.
+// - down: d-pad down button.
+// - left: d-pad left button.
+// - right: d-pad right button.
+// - start: rightmost button at bottom of device.
+// - select: second to the right at bottom of device.
+// - home: leftmost button at bottom of device.
+// - screen: brightness control button at bottom of device.
+// - note: mute control button at bottom of device.
+// - ltrigger: left shoulder trigger button.
+// - rtrigger: right shoulder trigger button.
+// - hold: hold setting of power switch.
+// - wlan: wireless networking switch.
+// - remote_hold: hold switch on headset.
+// - vol_up: volume up button next to home at bottom of device.
+// - vol_down: volume down button next to home at bottom of device.
+// - disc: UMD disc sensor.
+// - memstick: memory stick sensor.
+// - forward: forward button on headset.
+// - back: back button on headset.
+// - playpause: play/pause button on headset.
+//
+// Empty response.
+void WebSocketInputState::Buttons(DebuggerRequest &req) {
+ const JsonNode *jsonButtons = req.data.get("buttons");
+ if (!jsonButtons) {
+ return req.Fail("Missing 'buttons' parameter");
+ }
+ if (jsonButtons->value.getTag() != JSON_OBJECT) {
+ return req.Fail("Invalid 'buttons' parameter type");
+ }
+
+ uint32_t downFlags = 0;
+ uint32_t upFlags = 0;
+
+ for (const JsonNode *button : jsonButtons->value) {
+ auto info = buttonLookup.find(button->key);
+ if (info == buttonLookup.end()) {
+ return req.Fail(StringFromFormat("Unsupported 'buttons' object key '%s'", button->key));
+ }
+ if (button->value.getTag() == JSON_TRUE) {
+ downFlags |= info->second;
+ } else if (button->value.getTag() == JSON_FALSE) {
+ upFlags |= info->second;
+ } else if (button->value.getTag() != JSON_NULL) {
+ return req.Fail(StringFromFormat("Unsupported 'buttons' object type for key '%s'", button->key));
+ }
+ }
+
+ if (downFlags) {
+ __CtrlButtonDown(downFlags);
+ }
+ if (upFlags) {
+ __CtrlButtonUp(upFlags);
+ }
+
+ req.Respond();
+}
+
+// Press and release a button (input.press)
+//
+// Parameters:
+// - button: required string indicating button name (see input.buttons.)
+// - duration: optional integer indicating frames to press for, defaults to 1.
+//
+// Empty response once released.
+void WebSocketInputState::Press(DebuggerRequest &req) {
+ std::string button;
+ if (!req.ParamString("button", &button))
+ return;
+
+ PressInfo press;
+ press.duration = 1;
+ if (!req.ParamU32("duration", &press.duration, false, DebuggerParamType::OPTIONAL))
+ return;
+ if (press.duration < 0)
+ return req.Fail("Parameter 'duration' must not be negative");
+ const JsonNode *value = req.data.get("ticket");
+ press.ticket = value ? json_stringify(value) : "";
+
+ auto info = buttonLookup.find(button);
+ if (info == buttonLookup.end()) {
+ return req.Fail(StringFromFormat("Unsupported button value '%s'", button.c_str()));
+ }
+ press.button = info->second;
+
+ __CtrlButtonDown(press.button);
+ pressTickets_.push_back(press);
+}
+
+void WebSocketInputState::Broadcast(net::WebSocketServer *ws) {
+ int counter = __DisplayGetNumVblanks();
+ if (pressTickets_.empty() || lastCounter_ == counter)
+ return;
+ lastCounter_ = counter;
+
+ for (PressInfo &press : pressTickets_) {
+ press.duration--;
+ if (press.duration == -1) {
+ __CtrlButtonUp(press.button);
+ ws->Send(press.Event());
+ }
+ }
+ auto negative = [](const PressInfo &press) -> bool {
+ return press.duration < 0;
+ };
+ pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), negative), pressTickets_.end());
+}
+
+static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) {
+ const JsonNode *node = req.data.get(name);
+ if (!node) {
+ req.Fail(StringFromFormat("Missing '%s' parameter", name));
+ return false;
+ }
+ if (node->value.getTag() != JSON_NUMBER) {
+ req.Fail(StringFromFormat("Invalid '%s' parameter type", name));
+ return false;
+ }
+
+ double val = node->value.toNumber();
+ if (val < 1.0 || val > 1.0) {
+ req.Fail(StringFromFormat("Parameter '%s' must be between -1.0 and 1.0", name));
+ return false;
+ }
+
+ *value = (float)val;
+ return true;
+}
+
+// Set coordinates of analog stick (input.analog)
+//
+// Parameters:
+// - x: required number from -1.0 to 1.0.
+// - y: required number from -1.0 to 1.0.
+// - stick: optional string, either "left" (default) or "right".
+//
+// Empty response.
+void WebSocketInputState::Analog(DebuggerRequest &req) {
+ std::string stick = "left";
+ if (!req.ParamString("stick", &stick, DebuggerParamType::OPTIONAL))
+ return;
+ if (stick != "left" && stick != "right")
+ return req.Fail(StringFromFormat("Parameter 'stick' must be 'left' or 'right', not '%s'", stick.c_str()));
+ float x, y;
+ if (!AnalogValue(req, &x, "x") || !AnalogValue(req, &y, "y"))
+ return;
+
+ __CtrlSetAnalogX(x, stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT);
+ __CtrlSetAnalogY(y, stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT);
+
+ req.Respond();
+}
diff --git a/Core/Debugger/WebSocket/InputSubscriber.h b/Core/Debugger/WebSocket/InputSubscriber.h
new file mode 100644
index 0000000000..ed67ae659d
--- /dev/null
+++ b/Core/Debugger/WebSocket/InputSubscriber.h
@@ -0,0 +1,22 @@
+// Copyright (c) 2021- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#pragma once
+
+#include "Core/Debugger/WebSocket/WebSocketUtils.h"
+
+DebuggerSubscriber *WebSocketInputInit(DebuggerEventHandlerMap &map);
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index 2e46086a0b..77fff29b39 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -399,6 +399,7 @@
+
@@ -629,6 +630,7 @@
+
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters
index ea66a38598..06db4d5876 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj.filters
+++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters
@@ -682,6 +682,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
Debugger\WebSocket
@@ -1488,6 +1491,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
Debugger\WebSocket
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 4579fd9b77..ef935455d6 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -413,6 +413,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/GPURecordSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/HLESubscriber.cpp \
+ $(SRC)/Core/Debugger/WebSocket/InputSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/LogBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/MemorySubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/SteppingBroadcaster.cpp \