Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Core/Debugger/WebSocket/InputSubscriber.cpp
3187 views
1
// Copyright (c) 2021- PPSSPP Project.
2
3
// This program is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, version 2.0 or later versions.
6
7
// This program is distributed in the hope that it will be useful,
8
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
// GNU General Public License 2.0 for more details.
11
12
// A copy of the GPL 2.0 should have been included with the program.
13
// If not, see http://www.gnu.org/licenses/
14
15
// Official git repository and contact information can be found at
16
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17
18
#include <algorithm>
19
#include <unordered_map>
20
21
#include "Common/StringUtils.h"
22
#include "Core/Debugger/WebSocket/InputSubscriber.h"
23
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
24
#include "Core/HLE/sceCtrl.h"
25
#include "Core/HW/Display.h"
26
27
// This is also used in InputBroadcaster.
28
const std::unordered_map<std::string, uint32_t> buttonLookup = {
29
{ "cross", CTRL_CROSS },
30
{ "circle", CTRL_CIRCLE },
31
{ "triangle", CTRL_TRIANGLE },
32
{ "square", CTRL_SQUARE },
33
{ "up", CTRL_UP },
34
{ "down", CTRL_DOWN },
35
{ "left", CTRL_LEFT },
36
{ "right", CTRL_RIGHT },
37
{ "start", CTRL_START },
38
{ "select", CTRL_SELECT },
39
{ "home", CTRL_HOME },
40
{ "screen", CTRL_SCREEN },
41
{ "note", CTRL_NOTE },
42
{ "ltrigger", CTRL_LTRIGGER },
43
{ "rtrigger", CTRL_RTRIGGER },
44
{ "hold", CTRL_HOLD },
45
{ "wlan", CTRL_WLAN },
46
{ "remote_hold", CTRL_REMOTE_HOLD },
47
{ "vol_up", CTRL_VOL_UP },
48
{ "vol_down", CTRL_VOL_DOWN },
49
{ "disc", CTRL_DISC },
50
{ "memstick", CTRL_MEMSTICK },
51
{ "forward", CTRL_FORWARD },
52
{ "back", CTRL_BACK },
53
{ "playpause", CTRL_PLAYPAUSE },
54
// Obscure unmapped keys, see issue #17464
55
{ "l2", CTRL_L2 },
56
{ "l3", CTRL_L3 },
57
{ "r2", CTRL_R2 },
58
{ "r3", CTRL_R3 },
59
};
60
61
struct WebSocketInputState : public DebuggerSubscriber {
62
void ButtonsSend(DebuggerRequest &req);
63
void ButtonsPress(DebuggerRequest &req);
64
void AnalogSend(DebuggerRequest &req);
65
66
void Broadcast(net::WebSocketServer *ws) override;
67
68
protected:
69
struct PressInfo {
70
std::string ticket;
71
uint32_t button;
72
uint32_t duration;
73
74
std::string Event();
75
};
76
77
std::vector<PressInfo> pressTickets_;
78
int lastCounter_ = -1;
79
};
80
81
std::string WebSocketInputState::PressInfo::Event() {
82
JsonWriter j;
83
j.begin();
84
j.writeString("event", "input.buttons.press");
85
if (!ticket.empty()) {
86
j.writeRaw("ticket", ticket);
87
}
88
j.end();
89
return j.str();
90
}
91
92
const std::unordered_map<std::string, uint32_t> &WebSocketInputButtonLookup() {
93
return buttonLookup;
94
}
95
96
DebuggerSubscriber *WebSocketInputInit(DebuggerEventHandlerMap &map) {
97
auto p = new WebSocketInputState();
98
map["input.buttons.send"] = std::bind(&WebSocketInputState::ButtonsSend, p, std::placeholders::_1);
99
map["input.buttons.press"] = std::bind(&WebSocketInputState::ButtonsPress, p, std::placeholders::_1);
100
map["input.analog.send"] = std::bind(&WebSocketInputState::AnalogSend, p, std::placeholders::_1);
101
102
return p;
103
}
104
105
// Alter PSP button press flags (input.buttons.send)
106
//
107
// Parameters:
108
// - buttons: object containing button names as string keys, boolean press state as value.
109
//
110
// Button names (some are not respected by PPSSPP):
111
// - cross: button on bottom side of right pad.
112
// - circle: button on right side of right pad.
113
// - triangle: button on top side of right pad.
114
// - square: button on left side of right pad.
115
// - up: d-pad up button.
116
// - down: d-pad down button.
117
// - left: d-pad left button.
118
// - right: d-pad right button.
119
// - start: rightmost button at bottom of device.
120
// - select: second to the right at bottom of device.
121
// - home: leftmost button at bottom of device.
122
// - screen: brightness control button at bottom of device.
123
// - note: mute control button at bottom of device.
124
// - ltrigger: left shoulder trigger button.
125
// - rtrigger: right shoulder trigger button.
126
// - hold: hold setting of power switch.
127
// - wlan: wireless networking switch.
128
// - remote_hold: hold switch on headset.
129
// - vol_up: volume up button next to home at bottom of device.
130
// - vol_down: volume down button next to home at bottom of device.
131
// - disc: UMD disc sensor.
132
// - memstick: memory stick sensor.
133
// - forward: forward button on headset.
134
// - back: back button on headset.
135
// - playpause: play/pause button on headset.
136
//
137
// Response (same event name) with no extra data.
138
void WebSocketInputState::ButtonsSend(DebuggerRequest &req) {
139
const JsonNode *jsonButtons = req.data.get("buttons");
140
if (!jsonButtons) {
141
return req.Fail("Missing 'buttons' parameter");
142
}
143
if (jsonButtons->value.getTag() != JSON_OBJECT) {
144
return req.Fail("Invalid 'buttons' parameter type");
145
}
146
147
uint32_t downFlags = 0;
148
uint32_t upFlags = 0;
149
150
for (const JsonNode *button : jsonButtons->value) {
151
auto info = buttonLookup.find(button->key);
152
if (info == buttonLookup.end()) {
153
return req.Fail(StringFromFormat("Unsupported 'buttons' object key '%s'", button->key));
154
}
155
if (button->value.getTag() == JSON_TRUE) {
156
downFlags |= info->second;
157
} else if (button->value.getTag() == JSON_FALSE) {
158
upFlags |= info->second;
159
} else if (button->value.getTag() != JSON_NULL) {
160
return req.Fail(StringFromFormat("Unsupported 'buttons' object type for key '%s'", button->key));
161
}
162
}
163
164
__CtrlUpdateButtons(downFlags, upFlags);
165
166
req.Respond();
167
}
168
169
// Press and release a button (input.buttons.press)
170
//
171
// Parameters:
172
// - button: required string indicating button name (see input.buttons.send.)
173
// - duration: optional integer indicating frames to press for, defaults to 1.
174
//
175
// Response (same event name) with no extra data once released.
176
void WebSocketInputState::ButtonsPress(DebuggerRequest &req) {
177
std::string button;
178
if (!req.ParamString("button", &button))
179
return;
180
181
PressInfo press;
182
press.duration = 1;
183
if (!req.ParamU32("duration", &press.duration, false, DebuggerParamType::OPTIONAL))
184
return;
185
if ((int)press.duration < 0)
186
return req.Fail("Parameter 'duration' must not be negative");
187
const JsonNode *value = req.data.get("ticket");
188
press.ticket = value ? json_stringify(value) : "";
189
190
auto info = buttonLookup.find(button);
191
if (info == buttonLookup.end()) {
192
return req.Fail(StringFromFormat("Unsupported button value '%s'", button.c_str()));
193
}
194
press.button = info->second;
195
196
__CtrlUpdateButtons(press.button, 0);
197
pressTickets_.push_back(press);
198
}
199
200
void WebSocketInputState::Broadcast(net::WebSocketServer *ws) {
201
int counter = __DisplayGetNumVblanks();
202
if (pressTickets_.empty() || lastCounter_ == counter)
203
return;
204
lastCounter_ = counter;
205
206
for (PressInfo &press : pressTickets_) {
207
press.duration--;
208
if (press.duration == -1) {
209
__CtrlUpdateButtons(0, press.button);
210
ws->Send(press.Event());
211
}
212
}
213
auto negative = [](const PressInfo &press) -> bool {
214
return press.duration < 0;
215
};
216
pressTickets_.erase(std::remove_if(pressTickets_.begin(), pressTickets_.end(), negative), pressTickets_.end());
217
}
218
219
static bool AnalogValue(DebuggerRequest &req, float *value, const char *name) {
220
const JsonNode *node = req.data.get(name);
221
if (!node) {
222
req.Fail(StringFromFormat("Missing '%s' parameter", name));
223
return false;
224
}
225
if (node->value.getTag() != JSON_NUMBER) {
226
req.Fail(StringFromFormat("Invalid '%s' parameter type", name));
227
return false;
228
}
229
230
double val = node->value.toNumber();
231
if (val < -1.0 || val > 1.0) {
232
req.Fail(StringFromFormat("Parameter '%s' must be between -1.0 and 1.0", name));
233
return false;
234
}
235
236
*value = (float)val;
237
return true;
238
}
239
240
// Set coordinates of analog stick (input.analog.send)
241
//
242
// Parameters:
243
// - x: required number from -1.0 to 1.0.
244
// - y: required number from -1.0 to 1.0.
245
// - stick: optional string, either "left" (default) or "right".
246
//
247
// Response (same event name) with no extra data.
248
void WebSocketInputState::AnalogSend(DebuggerRequest &req) {
249
std::string stick = "left";
250
if (!req.ParamString("stick", &stick, DebuggerParamType::OPTIONAL))
251
return;
252
if (stick != "left" && stick != "right")
253
return req.Fail(StringFromFormat("Parameter 'stick' must be 'left' or 'right', not '%s'", stick.c_str()));
254
float x, y;
255
if (!AnalogValue(req, &x, "x") || !AnalogValue(req, &y, "y"))
256
return;
257
258
// TODO: Route into the control mapper's PSPKey function or similar instead.
259
__CtrlSetAnalogXY(stick == "left" ? CTRL_STICK_LEFT : CTRL_STICK_RIGHT, x, y);
260
261
req.Respond();
262
}
263
264