drgame-cc 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,809 @@
1
+ import VirtualGamepadClient, { VirtualGamepadConnectionState } from "./VirtualGamepadClient";
2
+ import VirtualGamepadConfig, {
3
+ VIRTUAL_GAMEPAD_GAME_ID,
4
+ VIRTUAL_GAMEPAD_MAX_MEMBERS,
5
+ VIRTUAL_GAMEPAD_PLAYER_ID,
6
+ VIRTUAL_GAMEPAD_WXACODE_PAGE,
7
+ VIRTUAL_GAMEPAD_WXACODE_PROJECT_CODE
8
+ } from "./VirtualGamepadConfig";
9
+ // import { GameVoManager } from "../manager/GameVoManager";
10
+
11
+ export type VirtualGamepadGameState = "ready" | "playing" | "paused" | "ended";
12
+ export type VirtualGamepadUserType = "xiaomi" | "guest";
13
+ export type VirtualGamepadSunCodeState = "idle" | "loading" | "ready" | "error";
14
+
15
+ export interface VirtualGamepadIdentity {
16
+ userId: string;
17
+ userType: VirtualGamepadUserType;
18
+ guestId?: string;
19
+ }
20
+
21
+ export interface VirtualGamepadMoveInput {
22
+ x: number;
23
+ y: number;
24
+ seq?: number;
25
+ ts?: number;
26
+ raw?: any;
27
+ holdMode?: boolean;
28
+ stopTimeoutMs?: number;
29
+ side?: string;
30
+ direction?: string;
31
+ action?: string;
32
+ }
33
+
34
+ export type VirtualGamepadControlType = "stick" | "button" | "shoulder" | "back";
35
+
36
+ export interface VirtualGamepadControlInput {
37
+ type: VirtualGamepadControlType;
38
+ action: string;
39
+ pressed: boolean;
40
+ side?: string;
41
+ key?: string;
42
+ direction?: string;
43
+ x?: number;
44
+ y?: number;
45
+ playerId?: string;
46
+ seq?: number;
47
+ ts?: number;
48
+ raw?: any;
49
+ }
50
+
51
+ export interface VirtualGamepadStatus {
52
+ connectionState: VirtualGamepadConnectionState;
53
+ controllerConnected: boolean;
54
+ roomId: string;
55
+ deviceId: string;
56
+ userId: string;
57
+ userType: VirtualGamepadUserType;
58
+ identityText: string;
59
+ hasToken: boolean;
60
+ maxMembers: number;
61
+ sunCodeState: VirtualGamepadSunCodeState;
62
+ sunCodeDataUrl: string;
63
+ sunCodeError?: string;
64
+ h5Url: string;
65
+ qrPayload: string;
66
+ statusText: string;
67
+ lastError?: string;
68
+ }
69
+
70
+ type MoveListener = (input: VirtualGamepadMoveInput) => void;
71
+ type ConfirmListener = (pressed: boolean, raw?: any) => void;
72
+ type StatusListener = (status: VirtualGamepadStatus) => void;
73
+ type ControlListener = (input: VirtualGamepadControlInput) => void;
74
+
75
+ export default class VirtualGamepadManager {
76
+ private static instance: VirtualGamepadManager | null = null;
77
+
78
+ public static getInstance(): VirtualGamepadManager {
79
+ let instance = this.instance;
80
+ if (!instance) {
81
+ instance = new VirtualGamepadManager();
82
+ this.instance = instance;
83
+ }
84
+ return instance;
85
+ }
86
+
87
+ private client: VirtualGamepadClient | null = null;
88
+ private roomId: string = "";
89
+ private deviceId: string = "";
90
+ private identity: VirtualGamepadIdentity = { userId: "", userType: "guest" };
91
+ private started: boolean = false;
92
+ private connectionState: VirtualGamepadConnectionState = "idle";
93
+ private controllerConnected: boolean = false;
94
+ private lastError: string = "";
95
+ private lastInputSeq: number = -1;
96
+ private gameState: VirtualGamepadGameState = "ready";
97
+ private sunCodeState: VirtualGamepadSunCodeState = "idle";
98
+ private sunCodeDataUrl: string = "";
99
+ private sunCodeError: string = "";
100
+ private sunCodeRequestSerial: number = 0;
101
+ private moveListeners: MoveListener[] = [];
102
+ private confirmListeners: ConfirmListener[] = [];
103
+ private statusListeners: StatusListener[] = [];
104
+ private controlListeners: ControlListener[] = [];
105
+ private stickListeners: ControlListener[] = [];
106
+ private buttonListeners: ControlListener[] = [];
107
+ private shoulderListeners: ControlListener[] = [];
108
+ private backListeners: ControlListener[] = [];
109
+
110
+ private constructor() {
111
+ this.deviceId = this.getOrCreateDeviceId();
112
+ this.identity = this.resolveIdentity();
113
+ this.roomId = VirtualGamepadConfig.getRoomId() || this.identity.userId || this.createRoomId();
114
+ }
115
+
116
+ public start() {
117
+ if (this.started) {
118
+ this.emitStatus();
119
+ return;
120
+ }
121
+ this.started = true;
122
+ this.controllerConnected = false;
123
+ this.lastInputSeq = -1;
124
+ this.lastError = "";
125
+ this.identity = this.resolveIdentity();
126
+ let configuredRoomId = VirtualGamepadConfig.getRoomId();
127
+ this.roomId = configuredRoomId || this.identity.userId || this.createRoomId();
128
+ if (configuredRoomId) {
129
+ console.warn("[VirtualGamepad][Identity] roomId uses runtime override", {
130
+ roomId: configuredRoomId,
131
+ configKey: "RXQS_GAMEPAD_ROOM_ID"
132
+ });
133
+ }
134
+ let token = VirtualGamepadConfig.getToken();
135
+ this.client = new VirtualGamepadClient();
136
+ this.client.onStateChange = this.handleClientState.bind(this);
137
+ this.client.onMessage = this.handleMessage.bind(this);
138
+ this.client.connect({
139
+ wsUrl: VirtualGamepadConfig.getWsUrl(),
140
+ roomId: this.roomId,
141
+ deviceId: this.deviceId,
142
+ game: VIRTUAL_GAMEPAD_GAME_ID,
143
+ userId: this.identity.userId,
144
+ userType: this.identity.userType,
145
+ guestId: this.identity.guestId,
146
+ token: token,
147
+ maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS
148
+ });
149
+ console.log("[VirtualGamepad][Identity] connect", {
150
+ environment: VirtualGamepadConfig.getEnvironment(),
151
+ userType: this.identity.userType,
152
+ userId: this.maskUserId(this.identity.userId),
153
+ roomId: this.roomId,
154
+ hasToken: !!token,
155
+ maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS
156
+ });
157
+ this.requestSunCode();
158
+ this.emitStatus();
159
+ }
160
+
161
+ public stop() {
162
+ this.started = false;
163
+ this.controllerConnected = false;
164
+ this.lastInputSeq = -1;
165
+ this.sunCodeRequestSerial++;
166
+ if (this.client) {
167
+ this.client.close();
168
+ this.client = null;
169
+ }
170
+ this.connectionState = "closed";
171
+ this.emitStatus();
172
+ }
173
+
174
+ public setGameState(state: VirtualGamepadGameState) {
175
+ this.gameState = state;
176
+ }
177
+
178
+ public getStatus(): VirtualGamepadStatus {
179
+ return {
180
+ connectionState: this.connectionState,
181
+ controllerConnected: this.controllerConnected,
182
+ roomId: this.roomId,
183
+ deviceId: this.deviceId,
184
+ userId: this.identity.userId,
185
+ userType: this.identity.userType,
186
+ identityText: this.getIdentityText(),
187
+ hasToken: !!VirtualGamepadConfig.getToken(),
188
+ maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS,
189
+ sunCodeState: this.sunCodeState,
190
+ sunCodeDataUrl: this.sunCodeDataUrl,
191
+ sunCodeError: this.sunCodeError,
192
+ h5Url: this.getH5Url(),
193
+ qrPayload: this.getQrPayload(),
194
+ statusText: this.getStatusText(),
195
+ lastError: this.lastError
196
+ };
197
+ }
198
+
199
+ public getQrPayload(): string {
200
+ if (VirtualGamepadConfig.getQrMode() == "json") {
201
+ return JSON.stringify({
202
+ wsUrl: VirtualGamepadConfig.getWsUrl(),
203
+ roomId: this.roomId,
204
+ maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS,
205
+ token: VirtualGamepadConfig.getToken()
206
+ });
207
+ }
208
+ return this.getH5Url();
209
+ }
210
+
211
+ public getH5Url(): string {
212
+ return this.appendParams(VirtualGamepadConfig.getH5Url(), {
213
+ game: VIRTUAL_GAMEPAD_GAME_ID,
214
+ roomId: this.roomId,
215
+ wsUrl: VirtualGamepadConfig.getWsUrl(),
216
+ playerId: VIRTUAL_GAMEPAD_PLAYER_ID,
217
+ maxMembers: String(VIRTUAL_GAMEPAD_MAX_MEMBERS),
218
+ token: VirtualGamepadConfig.getToken()
219
+ });
220
+ }
221
+
222
+ public onMove(listener: MoveListener) {
223
+ if (listener && this.moveListeners.indexOf(listener) < 0) {
224
+ this.moveListeners.push(listener);
225
+ }
226
+ }
227
+
228
+ public offMove(listener: MoveListener) {
229
+ this.removeListener(this.moveListeners, listener);
230
+ }
231
+
232
+ public onConfirm(listener: ConfirmListener) {
233
+ if (listener && this.confirmListeners.indexOf(listener) < 0) {
234
+ this.confirmListeners.push(listener);
235
+ }
236
+ }
237
+
238
+ public offConfirm(listener: ConfirmListener) {
239
+ this.removeListener(this.confirmListeners, listener);
240
+ }
241
+
242
+ public onStatus(listener: StatusListener) {
243
+ if (listener && this.statusListeners.indexOf(listener) < 0) {
244
+ this.statusListeners.push(listener);
245
+ }
246
+ }
247
+
248
+ public offStatus(listener: StatusListener) {
249
+ this.removeListener(this.statusListeners, listener);
250
+ }
251
+
252
+ public onControl(listener: ControlListener) {
253
+ if (listener && this.controlListeners.indexOf(listener) < 0) {
254
+ this.controlListeners.push(listener);
255
+ }
256
+ }
257
+
258
+ public offControl(listener: ControlListener) {
259
+ this.removeListener(this.controlListeners, listener);
260
+ }
261
+
262
+ public onStick(listener: ControlListener) {
263
+ if (listener && this.stickListeners.indexOf(listener) < 0) {
264
+ this.stickListeners.push(listener);
265
+ }
266
+ }
267
+
268
+ public offStick(listener: ControlListener) {
269
+ this.removeListener(this.stickListeners, listener);
270
+ }
271
+
272
+ public onButton(listener: ControlListener) {
273
+ if (listener && this.buttonListeners.indexOf(listener) < 0) {
274
+ this.buttonListeners.push(listener);
275
+ }
276
+ }
277
+
278
+ public offButton(listener: ControlListener) {
279
+ this.removeListener(this.buttonListeners, listener);
280
+ }
281
+
282
+ public onShoulder(listener: ControlListener) {
283
+ if (listener && this.shoulderListeners.indexOf(listener) < 0) {
284
+ this.shoulderListeners.push(listener);
285
+ }
286
+ }
287
+
288
+ public offShoulder(listener: ControlListener) {
289
+ this.removeListener(this.shoulderListeners, listener);
290
+ }
291
+
292
+ public onBack(listener: ControlListener) {
293
+ if (listener && this.backListeners.indexOf(listener) < 0) {
294
+ this.backListeners.push(listener);
295
+ }
296
+ }
297
+
298
+ public offBack(listener: ControlListener) {
299
+ this.removeListener(this.backListeners, listener);
300
+ }
301
+
302
+ private handleClientState(state: VirtualGamepadConnectionState, reason?: string) {
303
+ this.connectionState = state;
304
+ if (state == "error") {
305
+ this.lastError = reason || "连接失败";
306
+ }
307
+ if (state == "closed" || state == "error") {
308
+ this.controllerConnected = false;
309
+ this.emitMove({ x: 0, y: 0 });
310
+ }
311
+ this.emitStatus();
312
+ }
313
+
314
+ private handleMessage(message: any) {
315
+ if (!this.isCurrentRoomMessage(message)) {
316
+ console.warn("[VirtualGamepad][WSS] ignored message from another room", {
317
+ expectedRoomId: this.roomId,
318
+ receivedRoomId: message && message.roomId,
319
+ type: message && message.type
320
+ });
321
+ return;
322
+ }
323
+ if (message.type == "error") {
324
+ this.lastError = message.message || message.error || "手柄服务返回错误";
325
+ this.connectionState = "error";
326
+ this.emitStatus();
327
+ return;
328
+ }
329
+ if (message.type == "pong") {
330
+ return;
331
+ }
332
+ if (message.type == "room_state") {
333
+ this.handleRoomState(message);
334
+ return;
335
+ }
336
+ if (this.isControllerJoinMessage(message)) {
337
+ console.log("[VirtualGamepad][WSS][Presence] controller joined", message);
338
+ this.setControllerConnected(true);
339
+ return;
340
+ }
341
+ if (this.isControllerLeaveMessage(message)) {
342
+ console.log("[VirtualGamepad][WSS][Presence] controller left", message);
343
+ this.setControllerConnected(false);
344
+ this.emitMove({ x: 0, y: 0, raw: message });
345
+ return;
346
+ }
347
+ if (message.type == "input" || message.action) {
348
+ this.handleInput(message);
349
+ }
350
+ }
351
+
352
+ private handleInput(message: any) {
353
+ let payload = message.input || message;
354
+ let playerId = this.getMessagePlayerId(message) || this.getMessagePlayerId(payload);
355
+ if (playerId && playerId != VIRTUAL_GAMEPAD_PLAYER_ID) return;
356
+ let seq = typeof payload.seq == "number" ? payload.seq : message.seq;
357
+ if (typeof seq == "number") {
358
+ if (seq <= this.lastInputSeq) {
359
+ return;
360
+ }
361
+ this.lastInputSeq = seq;
362
+ }
363
+ this.setControllerConnected(true);
364
+ if (payload && this.handleStandardInput(message, payload)) {
365
+ return;
366
+ }
367
+ if (payload.action == "move" || payload.action == "stop") {
368
+ this.emitMove({
369
+ x: this.clampAxis(payload.action == "stop" ? 0 : Number(payload.x) || 0),
370
+ y: this.clampAxis(payload.action == "stop" ? 0 : Number(payload.y) || 0),
371
+ seq: seq,
372
+ ts: this.getMessageTimestamp(payload) || this.getMessageTimestamp(message),
373
+ raw: message
374
+ });
375
+ return;
376
+ }
377
+ if (payload.action == "confirm") {
378
+ let control = this.createControlInput("button", message, {
379
+ type: "button",
380
+ key: "confirm",
381
+ action: "press",
382
+ pressed: payload.pressed !== false
383
+ });
384
+ this.emitControl(control);
385
+ this.emitSpecificControl(control);
386
+ this.emitConfirm(payload.pressed !== false, message);
387
+ }
388
+ }
389
+
390
+ private handleStandardInput(message: any, input: any): boolean {
391
+ if (!input) return false;
392
+ if (input.type == "joystick" || input.type == "joystick_stick") {
393
+ let side = this.normalizeKey(input.side || "left");
394
+ let action = this.getInputAction(input);
395
+ let pressed = this.isPressedAction(input);
396
+ let vector = this.directionToVector(pressed ? input.direction : "");
397
+ let control = this.createControlInput("stick", message, {
398
+ type: "stick",
399
+ action: action,
400
+ pressed: pressed,
401
+ side: side,
402
+ direction: input.direction || "",
403
+ x: vector.x,
404
+ y: vector.y
405
+ });
406
+ this.emitControl(control);
407
+ this.emitSpecificControl(control);
408
+ if (side == "left") {
409
+ let rawInput = message.input || message;
410
+ this.emitMove({
411
+ x: vector.x,
412
+ y: vector.y,
413
+ seq: typeof rawInput.seq == "number" ? rawInput.seq : message.seq,
414
+ ts: this.getMessageTimestamp(rawInput) || this.getMessageTimestamp(message),
415
+ raw: message,
416
+ holdMode: true,
417
+ stopTimeoutMs: 0,
418
+ side: side,
419
+ direction: input.direction || "",
420
+ action: action
421
+ });
422
+ }
423
+ return true;
424
+ }
425
+ if (input.type == "button") {
426
+ let key = this.normalizeKey(input.key);
427
+ let control = this.createControlInput(key == "back" ? "back" : "button", message, {
428
+ type: key == "back" ? "back" : "button",
429
+ action: this.getInputAction(input),
430
+ pressed: this.isPressedAction(input),
431
+ key: key
432
+ });
433
+ this.emitControl(control);
434
+ this.emitSpecificControl(control);
435
+ if (key == "confirm" || key == "a") {
436
+ this.emitConfirm(control.pressed, message);
437
+ }
438
+ return true;
439
+ }
440
+ if (input.type == "shoulder") {
441
+ let control = this.createControlInput("shoulder", message, {
442
+ type: "shoulder",
443
+ action: this.getInputAction(input),
444
+ pressed: this.isPressedAction(input),
445
+ key: this.normalizeKey(input.key)
446
+ });
447
+ this.emitControl(control);
448
+ this.emitSpecificControl(control);
449
+ return true;
450
+ }
451
+ if (input.type == "back") {
452
+ let control = this.createControlInput("back", message, {
453
+ type: "back",
454
+ action: this.getInputAction(input),
455
+ pressed: this.isPressedAction(input),
456
+ key: "back"
457
+ });
458
+ this.emitControl(control);
459
+ this.emitSpecificControl(control);
460
+ return true;
461
+ }
462
+ return false;
463
+ }
464
+
465
+ private createControlInput(type: VirtualGamepadControlType, message: any, data: any): VirtualGamepadControlInput {
466
+ let rawInput = message && message.input ? message.input : message;
467
+ return {
468
+ type: type,
469
+ action: data.action || "press",
470
+ pressed: data.pressed !== false,
471
+ side: data.side,
472
+ key: data.key,
473
+ direction: data.direction,
474
+ x: typeof data.x == "number" ? data.x : undefined,
475
+ y: typeof data.y == "number" ? data.y : undefined,
476
+ playerId: this.getMessagePlayerId(message) || this.getMessagePlayerId(rawInput) || VIRTUAL_GAMEPAD_PLAYER_ID,
477
+ seq: rawInput && typeof rawInput.seq == "number" ? rawInput.seq : message.seq,
478
+ ts: this.getMessageTimestamp(rawInput) || this.getMessageTimestamp(message),
479
+ raw: message
480
+ };
481
+ }
482
+
483
+ private getInputAction(input: any): string {
484
+ return input && input.action ? String(input.action).toLowerCase() : "press";
485
+ }
486
+
487
+ private isPressedAction(input: any): boolean {
488
+ let action = this.getInputAction(input);
489
+ return action != "release" && action != "up" && action != "stop" && (!input || input.pressed !== false);
490
+ }
491
+
492
+ private normalizeKey(key: any): string {
493
+ return String(key || "").toLowerCase();
494
+ }
495
+
496
+ private isCurrentRoomMessage(message: any): boolean {
497
+ return !message || !message.roomId || message.roomId == this.roomId;
498
+ }
499
+
500
+ private isControllerJoinMessage(message: any): boolean {
501
+ if (!message) return false;
502
+ if (message.role == "game") return false;
503
+ if (message.type == "device_join" && this.isTargetPlayerMessage(message)) return true;
504
+ if (message.role == "controller" && (message.type == "join" || message.type == "joined")) return true;
505
+ return ["peer_joined", "player_joined", "controller_joined", "controller_connected"].indexOf(message.type) >= 0;
506
+ }
507
+
508
+ private isControllerLeaveMessage(message: any): boolean {
509
+ if (!message) return false;
510
+ if (message.type == "device_leave" && this.isTargetPlayerMessage(message)) return true;
511
+ return ["peer_left", "player_left", "controller_left", "controller_disconnected", "leave"].indexOf(message.type) >= 0;
512
+ }
513
+
514
+ private handleRoomState(message: any) {
515
+ if (message.slots && message.slots.length) {
516
+ this.setControllerConnected(this.hasPlayerOneInList(message.slots));
517
+ return;
518
+ }
519
+ if (message.devices && message.devices.length) {
520
+ this.setControllerConnected(this.hasPlayerOneInList(message.devices));
521
+ return;
522
+ }
523
+ if (typeof message.playerCount == "number") {
524
+ this.setControllerConnected(message.playerCount > 0);
525
+ return;
526
+ }
527
+ if (message.players && message.players[VIRTUAL_GAMEPAD_PLAYER_ID]) {
528
+ this.setControllerConnected(true);
529
+ return;
530
+ }
531
+ if (message.devices && message.devices[VIRTUAL_GAMEPAD_PLAYER_ID]) {
532
+ this.setControllerConnected(true);
533
+ }
534
+ }
535
+
536
+ private isTargetPlayerMessage(message: any): boolean {
537
+ let playerId = this.getMessagePlayerId(message);
538
+ return !playerId || playerId == VIRTUAL_GAMEPAD_PLAYER_ID;
539
+ }
540
+
541
+ private getMessagePlayerId(message: any): string {
542
+ if (!message) return "";
543
+ if (message.playerId) return String(message.playerId);
544
+ if (message.from == "player1" || message.from == "player2") return String(message.from);
545
+ if (message.role == "player1" || message.role == "player2") return String(message.role);
546
+ return "";
547
+ }
548
+
549
+ private hasPlayerOneInList(list: any): boolean {
550
+ if (!list || !list.length) return false;
551
+ for (let i = 0; i < list.length; i++) {
552
+ let item = list[i];
553
+ if (item && item.role == VIRTUAL_GAMEPAD_PLAYER_ID && item.occupied !== false) {
554
+ return true;
555
+ }
556
+ }
557
+ return false;
558
+ }
559
+
560
+ private directionToVector(direction: string): { x: number, y: number } {
561
+ let value = String(direction || "").toLowerCase();
562
+ let x = 0;
563
+ let y = 0;
564
+ if (value.indexOf("left") >= 0) x -= 1;
565
+ if (value.indexOf("right") >= 0) x += 1;
566
+ if (value.indexOf("up") >= 0) y += 1;
567
+ if (value.indexOf("down") >= 0) y -= 1;
568
+ return { x: x, y: y };
569
+ }
570
+
571
+ private getMessageTimestamp(message: any): number {
572
+ if (typeof message.ts == "number") return message.ts;
573
+ if (typeof message.timestamp == "number") return message.timestamp;
574
+ return 0;
575
+ }
576
+
577
+ private setControllerConnected(connected: boolean) {
578
+ if (this.controllerConnected == connected) return;
579
+ this.controllerConnected = connected;
580
+ if (!connected) {
581
+ this.emitMove({ x: 0, y: 0 });
582
+ }
583
+ this.emitStatus();
584
+ }
585
+
586
+ private emitMove(input: VirtualGamepadMoveInput) {
587
+ for (let i = this.moveListeners.length - 1; i >= 0; i--) {
588
+ this.moveListeners[i](input);
589
+ }
590
+ }
591
+
592
+ private emitConfirm(pressed: boolean, raw?: any) {
593
+ for (let i = this.confirmListeners.length - 1; i >= 0; i--) {
594
+ this.confirmListeners[i](pressed, raw);
595
+ }
596
+ }
597
+
598
+ private emitControl(input: VirtualGamepadControlInput) {
599
+ for (let i = this.controlListeners.length - 1; i >= 0; i--) {
600
+ this.controlListeners[i](input);
601
+ }
602
+ }
603
+
604
+ private emitSpecificControl(input: VirtualGamepadControlInput) {
605
+ let list: ControlListener[] = [];
606
+ if (input.type == "stick") list = this.stickListeners;
607
+ if (input.type == "button") list = this.buttonListeners;
608
+ if (input.type == "shoulder") list = this.shoulderListeners;
609
+ if (input.type == "back") list = this.backListeners;
610
+ for (let i = list.length - 1; i >= 0; i--) {
611
+ list[i](input);
612
+ }
613
+ }
614
+
615
+ private emitStatus() {
616
+ let status = this.getStatus();
617
+ for (let i = this.statusListeners.length - 1; i >= 0; i--) {
618
+ this.statusListeners[i](status);
619
+ }
620
+ }
621
+
622
+ private getStatusText(): string {
623
+ if (this.controllerConnected) return "虚拟手柄已连接";
624
+ if (this.connectionState == "connecting") return "正在连接手柄服务";
625
+ if (this.connectionState == "open" || this.connectionState == "joined") return "等待手柄扫码连接";
626
+ if (this.connectionState == "error") return this.lastError || "手柄服务连接失败";
627
+ return "虚拟手柄未连接";
628
+ }
629
+
630
+ private appendParams(url: string, params: { [key: string]: string }): string {
631
+ let query = [];
632
+ for (let key in params) {
633
+ query.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key] || ""));
634
+ }
635
+ let queryString = query.join("&");
636
+ let hashIndex = url.indexOf("#");
637
+ if (hashIndex >= 0) {
638
+ let prefix = url.substring(0, hashIndex);
639
+ let hash = url.substring(hashIndex + 1);
640
+ let separator = hash.indexOf("?") >= 0 ? "&" : "?";
641
+ return prefix + "#" + hash + separator + queryString;
642
+ }
643
+ let separator = url.indexOf("?") >= 0 ? "&" : "?";
644
+ return url + separator + queryString;
645
+ }
646
+
647
+ private clampAxis(value: number): number {
648
+ if (value > 1) return 1;
649
+ if (value < -1) return -1;
650
+ return value;
651
+ }
652
+
653
+ private removeListener<T>(list: T[], listener: T) {
654
+ let index = list.indexOf(listener);
655
+ if (index >= 0) {
656
+ list.splice(index, 1);
657
+ }
658
+ }
659
+
660
+ private getOrCreateDeviceId(): string {
661
+ let key = "rxqs_virtual_gamepad_device_id";
662
+ try {
663
+ let localStorage = cc.sys && cc.sys.localStorage;
664
+ if (localStorage) {
665
+ let saved = localStorage.getItem(key);
666
+ if (saved) return saved;
667
+ let created = this.createId("game");
668
+ localStorage.setItem(key, created);
669
+ return created;
670
+ }
671
+ } catch (e) {
672
+ }
673
+ return this.createId("game");
674
+ }
675
+
676
+ private requestSunCode() {
677
+ let requestSerial = ++this.sunCodeRequestSerial;
678
+ this.sunCodeState = "loading";
679
+ this.sunCodeDataUrl = "";
680
+ this.sunCodeError = "";
681
+ this.emitStatus();
682
+
683
+ let xhr = new XMLHttpRequest();
684
+ xhr.timeout = 10000;
685
+ let wxacodeUrl = VirtualGamepadConfig.getWxacodeUrl();
686
+ xhr.open("POST", wxacodeUrl, true);
687
+ xhr.responseType = "arraybuffer";
688
+ xhr.setRequestHeader("Content-Type", "application/json");
689
+ xhr.onload = () => {
690
+ if (requestSerial != this.sunCodeRequestSerial || !this.started) return;
691
+ if (xhr.status < 200 || xhr.status >= 300 || !xhr.response) {
692
+ this.failSunCode("太阳码接口返回 " + xhr.status, requestSerial);
693
+ return;
694
+ }
695
+ let bytes = new Uint8Array(xhr.response as ArrayBuffer);
696
+ let mimeType = this.detectImageMimeType(bytes);
697
+ if (!mimeType) {
698
+ this.failSunCode("太阳码接口未返回有效图片", requestSerial);
699
+ return;
700
+ }
701
+ this.sunCodeDataUrl = "data:" + mimeType + ";base64," + this.encodeBase64(bytes);
702
+ this.sunCodeState = "ready";
703
+ this.sunCodeError = "";
704
+ console.log("[VirtualGamepad][SunCode] ready", {
705
+ environment: VirtualGamepadConfig.getEnvironment(),
706
+ url: wxacodeUrl,
707
+ roomId: this.roomId,
708
+ page: VIRTUAL_GAMEPAD_WXACODE_PAGE,
709
+ projectCode: VIRTUAL_GAMEPAD_WXACODE_PROJECT_CODE,
710
+ byteLength: bytes.length,
711
+ mimeType: mimeType
712
+ });
713
+ this.emitStatus();
714
+ };
715
+ xhr.onerror = () => this.failSunCode("太阳码接口网络错误", requestSerial);
716
+ xhr.ontimeout = () => this.failSunCode("太阳码接口请求超时", requestSerial);
717
+ xhr.send(JSON.stringify({
718
+ scene: this.roomId,
719
+ page: VIRTUAL_GAMEPAD_WXACODE_PAGE,
720
+ projectCode: VIRTUAL_GAMEPAD_WXACODE_PROJECT_CODE
721
+ }));
722
+ }
723
+
724
+ private failSunCode(message: string, requestSerial: number) {
725
+ if (requestSerial != this.sunCodeRequestSerial || !this.started) return;
726
+ this.sunCodeState = "error";
727
+ this.sunCodeDataUrl = "";
728
+ this.sunCodeError = message;
729
+ console.warn("[VirtualGamepad][SunCode] unavailable", {
730
+ roomId: this.roomId,
731
+ message: message
732
+ });
733
+ this.emitStatus();
734
+ }
735
+
736
+ private detectImageMimeType(bytes: Uint8Array): string {
737
+ if (bytes.length >= 3 && bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff) {
738
+ return "image/jpeg";
739
+ }
740
+ if (bytes.length >= 8 && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4e && bytes[3] == 0x47) {
741
+ return "image/png";
742
+ }
743
+ return "";
744
+ }
745
+
746
+ private encodeBase64(bytes: Uint8Array): string {
747
+ let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
748
+ let output = "";
749
+ for (let i = 0; i < bytes.length; i += 3) {
750
+ let first = bytes[i];
751
+ let second = i + 1 < bytes.length ? bytes[i + 1] : 0;
752
+ let third = i + 2 < bytes.length ? bytes[i + 2] : 0;
753
+ output += chars.charAt(first >> 2);
754
+ output += chars.charAt(((first & 3) << 4) | (second >> 4));
755
+ output += i + 1 < bytes.length ? chars.charAt(((second & 15) << 2) | (third >> 6)) : "=";
756
+ output += i + 2 < bytes.length ? chars.charAt(third & 63) : "=";
757
+ }
758
+ return output;
759
+ }
760
+
761
+ private resolveIdentity(): VirtualGamepadIdentity {
762
+ // try {
763
+ // let userVo = GameVoManager.getInstance.myUserVo;
764
+ // let userId = userVo && userVo.lvUid ? String(userVo.lvUid).trim() : "";
765
+ // if (userVo && userVo.isAuthorize == 1 && userId) {
766
+ // return { userId: userId, userType: "xiaomi" };
767
+ // }
768
+ // } catch (e) {
769
+ // }
770
+ // 注释掉的用户id,使用游客id
771
+ let guestId = this.getOrCreateGuestId();
772
+ return { userId: guestId, userType: "guest", guestId: guestId };
773
+ }
774
+
775
+ private getOrCreateGuestId(): string {
776
+ let key = "rxqs_virtual_gamepad_guest_id";
777
+ try {
778
+ let localStorage = cc.sys && cc.sys.localStorage;
779
+ if (localStorage) {
780
+ let saved = localStorage.getItem(key);
781
+ if (saved) return saved;
782
+ let created = this.createId("guest");
783
+ localStorage.setItem(key, created);
784
+ return created;
785
+ }
786
+ } catch (e) {
787
+ }
788
+ return this.createId("guest");
789
+ }
790
+
791
+ private getIdentityText(): string {
792
+ let label = this.identity.userType == "xiaomi" ? "小米用户" : "游客";
793
+ return label + " " + this.maskUserId(this.identity.userId);
794
+ }
795
+
796
+ private maskUserId(userId: string): string {
797
+ let value = String(userId || "");
798
+ if (value.length <= 8) return value;
799
+ return value.substring(0, 4) + "..." + value.substring(value.length - 4);
800
+ }
801
+
802
+ private createRoomId(): string {
803
+ return this.createId("rxqs");
804
+ }
805
+
806
+ private createId(prefix: string): string {
807
+ return prefix + "_" + Date.now().toString(36) + "_" + Math.floor(Math.random() * 100000).toString(36);
808
+ }
809
+ }