// 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/HW/Display.h" // This is also used in InputBroadcaster. 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 ButtonsSend(DebuggerRequest &req); void ButtonsPress(DebuggerRequest &req); void AnalogSend(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.buttons.press"); if (!ticket.empty()) { j.writeRaw("ticket", ticket); } j.end(); return j.str(); } const std::unordered_map &WebSocketInputButtonLookup() { return buttonLookup; } DebuggerSubscriber *WebSocketInputInit(DebuggerEventHandlerMap &map) { auto p = new WebSocketInputState(); map["input.buttons.send"] = std::bind(&WebSocketInputState::ButtonsSend, p, std::placeholders::_1); map["input.buttons.press"] = std::bind(&WebSocketInputState::ButtonsPress, p, std::placeholders::_1); map["input.analog.send"] = std::bind(&WebSocketInputState::AnalogSend, p, std::placeholders::_1); return p; } // Alter PSP button press flags (input.buttons.send) // // 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. // // Response (same event name) with no extra data. void WebSocketInputState::ButtonsSend(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)); } } __CtrlUpdateButtons(downFlags, upFlags); req.Respond(); } // Press and release a button (input.buttons.press) // // Parameters: // - button: required string indicating button name (see input.buttons.send.) // - duration: optional integer indicating frames to press for, defaults to 1. // // Response (same event name) with no extra data once released. void WebSocketInputState::ButtonsPress(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; __CtrlUpdateButtons(press.button, 0); 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) { __CtrlUpdateButtons(0, 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.send) // // 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". // // Response (same event name) with no extra data. void WebSocketInputState::AnalogSend(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; // TODO: Route into the control mapper's PSPKey function or similar instead. __CtrlSetAnalogXY(stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT, x, y); req.Respond(); }