CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/Core/Debugger/WebSocket/InputSubscriber.cpp
Views: 1401
// Copyright (c) 2021- PPSSPP Project.12// This program is free software: you can redistribute it and/or modify3// it under the terms of the GNU General Public License as published by4// the Free Software Foundation, version 2.0 or later versions.56// This program is distributed in the hope that it will be useful,7// but WITHOUT ANY WARRANTY; without even the implied warranty of8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the9// GNU General Public License 2.0 for more details.1011// A copy of the GPL 2.0 should have been included with the program.12// If not, see http://www.gnu.org/licenses/1314// Official git repository and contact information can be found at15// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.1617#include <algorithm>18#include <unordered_map>19#include "Common/StringUtils.h"20#include "Core/Debugger/WebSocket/InputSubscriber.h"21#include "Core/Debugger/WebSocket/WebSocketUtils.h"22#include "Core/HLE/sceCtrl.h"23#include "Core/HW/Display.h"2425// This is also used in InputBroadcaster.26const std::unordered_map<std::string, uint32_t> buttonLookup = {27{ "cross", CTRL_CROSS },28{ "circle", CTRL_CIRCLE },29{ "triangle", CTRL_TRIANGLE },30{ "square", CTRL_SQUARE },31{ "up", CTRL_UP },32{ "down", CTRL_DOWN },33{ "left", CTRL_LEFT },34{ "right", CTRL_RIGHT },35{ "start", CTRL_START },36{ "select", CTRL_SELECT },37{ "home", CTRL_HOME },38{ "screen", CTRL_SCREEN },39{ "note", CTRL_NOTE },40{ "ltrigger", CTRL_LTRIGGER },41{ "rtrigger", CTRL_RTRIGGER },42{ "hold", CTRL_HOLD },43{ "wlan", CTRL_WLAN },44{ "remote_hold", CTRL_REMOTE_HOLD },45{ "vol_up", CTRL_VOL_UP },46{ "vol_down", CTRL_VOL_DOWN },47{ "disc", CTRL_DISC },48{ "memstick", CTRL_MEMSTICK },49{ "forward", CTRL_FORWARD },50{ "back", CTRL_BACK },51{ "playpause", CTRL_PLAYPAUSE },52// Obscure unmapped keys, see issue #1746453{ "l2", CTRL_L2 },54{ "l3", CTRL_L3 },55{ "r2", CTRL_R2 },56{ "r3", CTRL_R3 },57};5859struct WebSocketInputState : public DebuggerSubscriber {60void ButtonsSend(DebuggerRequest &req);61void ButtonsPress(DebuggerRequest &req);62void AnalogSend(DebuggerRequest &req);6364void Broadcast(net::WebSocketServer *ws) override;6566protected:67struct PressInfo {68std::string ticket;69uint32_t button;70uint32_t duration;7172std::string Event();73};7475std::vector<PressInfo> pressTickets_;76int lastCounter_ = -1;77};7879std::string WebSocketInputState::PressInfo::Event() {80JsonWriter j;81j.begin();82j.writeString("event", "input.buttons.press");83if (!ticket.empty()) {84j.writeRaw("ticket", ticket);85}86j.end();87return j.str();88}8990const std::unordered_map<std::string, uint32_t> &WebSocketInputButtonLookup() {91return buttonLookup;92}9394DebuggerSubscriber *WebSocketInputInit(DebuggerEventHandlerMap &map) {95auto p = new WebSocketInputState();96map["input.buttons.send"] = std::bind(&WebSocketInputState::ButtonsSend, p, std::placeholders::_1);97map["input.buttons.press"] = std::bind(&WebSocketInputState::ButtonsPress, p, std::placeholders::_1);98map["input.analog.send"] = std::bind(&WebSocketInputState::AnalogSend, p, std::placeholders::_1);99100return p;101}102103// Alter PSP button press flags (input.buttons.send)104//105// Parameters:106// - buttons: object containing button names as string keys, boolean press state as value.107//108// Button names (some are not respected by PPSSPP):109// - cross: button on bottom side of right pad.110// - circle: button on right side of right pad.111// - triangle: button on top side of right pad.112// - square: button on left side of right pad.113// - up: d-pad up button.114// - down: d-pad down button.115// - left: d-pad left button.116// - right: d-pad right button.117// - start: rightmost button at bottom of device.118// - select: second to the right at bottom of device.119// - home: leftmost button at bottom of device.120// - screen: brightness control button at bottom of device.121// - note: mute control button at bottom of device.122// - ltrigger: left shoulder trigger button.123// - rtrigger: right shoulder trigger button.124// - hold: hold setting of power switch.125// - wlan: wireless networking switch.126// - remote_hold: hold switch on headset.127// - vol_up: volume up button next to home at bottom of device.128// - vol_down: volume down button next to home at bottom of device.129// - disc: UMD disc sensor.130// - memstick: memory stick sensor.131// - forward: forward button on headset.132// - back: back button on headset.133// - playpause: play/pause button on headset.134//135// Response (same event name) with no extra data.136void WebSocketInputState::ButtonsSend(DebuggerRequest &req) {137const JsonNode *jsonButtons = req.data.get("buttons");138if (!jsonButtons) {139return req.Fail("Missing 'buttons' parameter");140}141if (jsonButtons->value.getTag() != JSON_OBJECT) {142return req.Fail("Invalid 'buttons' parameter type");143}144145uint32_t downFlags = 0;146uint32_t upFlags = 0;147148for (const JsonNode *button : jsonButtons->value) {149auto info = buttonLookup.find(button->key);150if (info == buttonLookup.end()) {151return req.Fail(StringFromFormat("Unsupported 'buttons' object key '%s'", button->key));152}153if (button->value.getTag() == JSON_TRUE) {154downFlags |= info->second;155} else if (button->value.getTag() == JSON_FALSE) {156upFlags |= info->second;157} else if (button->value.getTag() != JSON_NULL) {158return req.Fail(StringFromFormat("Unsupported 'buttons' object type for key '%s'", button->key));159}160}161162__CtrlUpdateButtons(downFlags, upFlags);163164req.Respond();165}166167// Press and release a button (input.buttons.press)168//169// Parameters:170// - button: required string indicating button name (see input.buttons.send.)171// - duration: optional integer indicating frames to press for, defaults to 1.172//173// Response (same event name) with no extra data once released.174void WebSocketInputState::ButtonsPress(DebuggerRequest &req) {175std::string button;176if (!req.ParamString("button", &button))177return;178179PressInfo press;180press.duration = 1;181if (!req.ParamU32("duration", &press.duration, false, DebuggerParamType::OPTIONAL))182return;183if (press.duration < 0)184return req.Fail("Parameter 'duration' must not be negative");185const JsonNode *value = req.data.get("ticket");186press.ticket = value ? json_stringify(value) : "";187188auto info = buttonLookup.find(button);189if (info == buttonLookup.end()) {190return req.Fail(StringFromFormat("Unsupported button value '%s'", button.c_str()));191}192press.button = info->second;193194__CtrlUpdateButtons(press.button, 0);195pressTickets_.push_back(press);196}197198void WebSocketInputState::Broadcast(net::WebSocketServer *ws) {199int counter = __DisplayGetNumVblanks();200if (pressTickets_.empty() || lastCounter_ == counter)201return;202lastCounter_ = counter;203204for (PressInfo &press : pressTickets_) {205press.duration--;206if (press.duration == -1) {207__CtrlUpdateButtons(0, press.button);208ws->Send(press.Event());209}210}211auto negative = [](const PressInfo &press) -> bool {212return press.duration < 0;213};214pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), negative), pressTickets_.end());215}216217static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) {218const JsonNode *node = req.data.get(name);219if (!node) {220req.Fail(StringFromFormat("Missing '%s' parameter", name));221return false;222}223if (node->value.getTag() != JSON_NUMBER) {224req.Fail(StringFromFormat("Invalid '%s' parameter type", name));225return false;226}227228double val = node->value.toNumber();229if (val < -1.0 || val > 1.0) {230req.Fail(StringFromFormat("Parameter '%s' must be between -1.0 and 1.0", name));231return false;232}233234*value = (float)val;235return true;236}237238// Set coordinates of analog stick (input.analog.send)239//240// Parameters:241// - x: required number from -1.0 to 1.0.242// - y: required number from -1.0 to 1.0.243// - stick: optional string, either "left" (default) or "right".244//245// Response (same event name) with no extra data.246void WebSocketInputState::AnalogSend(DebuggerRequest &req) {247std::string stick = "left";248if (!req.ParamString("stick", &stick, DebuggerParamType::OPTIONAL))249return;250if (stick != "left" && stick != "right")251return req.Fail(StringFromFormat("Parameter 'stick' must be 'left' or 'right', not '%s'", stick.c_str()));252float x, y;253if (!AnalogValue(req, &x, "x") || !AnalogValue(req, &y, "y"))254return;255256// TODO: Route into the control mapper's PSPKey function or similar instead.257__CtrlSetAnalogXY(stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT, x, y);258259req.Respond();260}261262263