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 \