CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
hrydgard

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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