sa2kit 1.6.33 → 1.6.35

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.
Files changed (47) hide show
  1. package/dist/calendar/index.js +11 -8
  2. package/dist/calendar/index.js.map +1 -1
  3. package/dist/calendar/index.mjs +4 -1
  4. package/dist/calendar/index.mjs.map +1 -1
  5. package/dist/chunk-EGJPS7OL.mjs +98 -0
  6. package/dist/chunk-EGJPS7OL.mjs.map +1 -0
  7. package/dist/{chunk-EI27JKND.mjs → chunk-GMIUSZXC.mjs} +2 -2
  8. package/dist/{chunk-EI27JKND.mjs.map → chunk-GMIUSZXC.mjs.map} +1 -1
  9. package/dist/chunk-HHVDOIPV.js +105 -0
  10. package/dist/chunk-HHVDOIPV.js.map +1 -0
  11. package/dist/chunk-PMTY2AI4.js +1110 -0
  12. package/dist/chunk-PMTY2AI4.js.map +1 -0
  13. package/dist/{chunk-XGBE4SUV.js → chunk-SCDDMIF6.js} +2 -2
  14. package/dist/{chunk-XGBE4SUV.js.map → chunk-SCDDMIF6.js.map} +1 -1
  15. package/dist/chunk-SR4JFEHW.mjs +1071 -0
  16. package/dist/chunk-SR4JFEHW.mjs.map +1 -0
  17. package/dist/index.d.mts +26 -1
  18. package/dist/index.d.ts +26 -1
  19. package/dist/index.js +179 -109
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +4 -2
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/mikuFireworks3D/index.d.mts +174 -0
  24. package/dist/mikuFireworks3D/index.d.ts +174 -0
  25. package/dist/mikuFireworks3D/index.js +69 -0
  26. package/dist/mikuFireworks3D/index.js.map +1 -0
  27. package/dist/mikuFireworks3D/index.mjs +4 -0
  28. package/dist/mikuFireworks3D/index.mjs.map +1 -0
  29. package/dist/mikuFireworks3D/server/index.d.mts +146 -0
  30. package/dist/mikuFireworks3D/server/index.d.ts +146 -0
  31. package/dist/mikuFireworks3D/server/index.js +338 -0
  32. package/dist/mikuFireworks3D/server/index.js.map +1 -0
  33. package/dist/mikuFireworks3D/server/index.mjs +335 -0
  34. package/dist/mikuFireworks3D/server/index.mjs.map +1 -0
  35. package/dist/mikuFusionGame/index.d.mts +7 -2
  36. package/dist/mikuFusionGame/index.d.ts +7 -2
  37. package/dist/mikuFusionGame/index.js +141 -10
  38. package/dist/mikuFusionGame/index.js.map +1 -1
  39. package/dist/mikuFusionGame/index.mjs +141 -10
  40. package/dist/mikuFusionGame/index.mjs.map +1 -1
  41. package/dist/mmd/index.js +1 -1
  42. package/dist/mmd/index.mjs +2 -2
  43. package/dist/types-Cgk9zWhO.d.mts +70 -0
  44. package/dist/types-Cgk9zWhO.d.ts +70 -0
  45. package/dist/universalFile/server/index.js +5 -5
  46. package/dist/universalFile/server/index.mjs +1 -1
  47. package/package.json +11 -1
@@ -0,0 +1,335 @@
1
+ import '../../chunk-BJTO5JO5.mjs';
2
+
3
+ // src/mikuFireworks3D/server/FireworksRoomHub.ts
4
+ var DEFAULT_MAX_USERS_PER_ROOM = 200;
5
+ var DEFAULT_HISTORY_LIMIT = 50;
6
+ var DEFAULT_MAX_DANMAKU_LENGTH = 64;
7
+ var FireworksRoomHub = class {
8
+ constructor(options) {
9
+ this.rooms = /* @__PURE__ */ new Map();
10
+ this.sessions = /* @__PURE__ */ new Map();
11
+ this.maxUsersPerRoom = options?.maxUsersPerRoom ?? DEFAULT_MAX_USERS_PER_ROOM;
12
+ this.historyLimit = options?.historyLimit ?? DEFAULT_HISTORY_LIMIT;
13
+ this.maxDanmakuLength = options?.maxDanmakuLength ?? DEFAULT_MAX_DANMAKU_LENGTH;
14
+ this.logger = options?.logger;
15
+ }
16
+ connect(transport, id) {
17
+ const connectionId = id ?? createId("conn");
18
+ const session = {
19
+ id: connectionId,
20
+ transport
21
+ };
22
+ this.sessions.set(connectionId, session);
23
+ return {
24
+ id: connectionId,
25
+ handleMessage: (raw) => this.handleRawMessage(connectionId, raw),
26
+ disconnect: (reason) => this.disconnect(connectionId, reason),
27
+ send: (message) => this.sendToConnection(connectionId, message)
28
+ };
29
+ }
30
+ disconnect(connectionId, reason) {
31
+ const session = this.sessions.get(connectionId);
32
+ if (!session) {
33
+ return;
34
+ }
35
+ this.leaveRoom(session, false);
36
+ this.sessions.delete(connectionId);
37
+ if (reason) {
38
+ this.logger?.info?.(`[FireworksRoomHub] disconnected ${connectionId}: ${reason}`);
39
+ }
40
+ }
41
+ getStats() {
42
+ return {
43
+ rooms: this.rooms.size,
44
+ connections: this.sessions.size
45
+ };
46
+ }
47
+ getRoomOnlineCount(roomId) {
48
+ return this.rooms.get(roomId)?.connections.size ?? 0;
49
+ }
50
+ handleRawMessage(connectionId, raw) {
51
+ const message = parseClientMessage(raw);
52
+ if (!message.ok) {
53
+ this.sendError(connectionId, "BAD_MESSAGE", message.error);
54
+ return;
55
+ }
56
+ const session = this.sessions.get(connectionId);
57
+ if (!session) {
58
+ return;
59
+ }
60
+ const clientMessage = message.value;
61
+ switch (clientMessage.type) {
62
+ case "join": {
63
+ this.joinRoom(session, clientMessage.roomId, clientMessage.user);
64
+ return;
65
+ }
66
+ case "leave": {
67
+ this.leaveRoom(session, true);
68
+ return;
69
+ }
70
+ case "danmaku.send": {
71
+ this.handleDanmaku(session, clientMessage.payload);
72
+ return;
73
+ }
74
+ case "firework.launch": {
75
+ this.handleFirework(session, clientMessage.payload);
76
+ return;
77
+ }
78
+ case "ping": {
79
+ this.sendToConnection(connectionId, {
80
+ type: "pong",
81
+ ts: clientMessage.ts ?? Date.now()
82
+ });
83
+ return;
84
+ }
85
+ default: {
86
+ this.sendError(connectionId, "UNSUPPORTED_MESSAGE", "Unsupported message type.");
87
+ }
88
+ }
89
+ }
90
+ joinRoom(session, roomId, user) {
91
+ const normalizedRoomId = roomId.trim();
92
+ const normalizedUserId = user.userId.trim();
93
+ if (!normalizedRoomId) {
94
+ this.sendError(session.id, "ROOM_ID_REQUIRED", "roomId is required.");
95
+ return;
96
+ }
97
+ if (!normalizedUserId) {
98
+ this.sendError(session.id, "USER_ID_REQUIRED", "user.userId is required.");
99
+ return;
100
+ }
101
+ if (session.roomId && session.roomId !== normalizedRoomId) {
102
+ this.leaveRoom(session, true);
103
+ }
104
+ const room = this.rooms.get(normalizedRoomId) ?? this.createRoom(normalizedRoomId);
105
+ if (room.connections.size >= this.maxUsersPerRoom && !room.connections.has(session.id)) {
106
+ this.sendError(session.id, "ROOM_FULL", "Room is full.");
107
+ return;
108
+ }
109
+ const identity = {
110
+ userId: normalizedUserId,
111
+ nickname: user.nickname,
112
+ avatarUrl: user.avatarUrl
113
+ };
114
+ const wasMember = room.connections.has(session.id);
115
+ room.connections.add(session.id);
116
+ room.usersByConnection.set(session.id, identity);
117
+ session.roomId = normalizedRoomId;
118
+ session.user = identity;
119
+ this.sendToConnection(session.id, {
120
+ type: "joined",
121
+ roomId: normalizedRoomId,
122
+ self: identity,
123
+ onlineCount: room.connections.size
124
+ });
125
+ this.sendToConnection(session.id, {
126
+ type: "room.snapshot",
127
+ roomId: normalizedRoomId,
128
+ users: Array.from(room.usersByConnection.values()),
129
+ danmakuHistory: [...room.danmakuHistory],
130
+ fireworkHistory: [...room.fireworkHistory]
131
+ });
132
+ if (!wasMember) {
133
+ this.broadcastToRoom(normalizedRoomId, {
134
+ type: "room.user_joined",
135
+ roomId: normalizedRoomId,
136
+ user: identity,
137
+ onlineCount: room.connections.size
138
+ }, session.id);
139
+ }
140
+ }
141
+ leaveRoom(session, notify) {
142
+ const roomId = session.roomId;
143
+ if (!roomId) {
144
+ return;
145
+ }
146
+ const room = this.rooms.get(roomId);
147
+ if (!room) {
148
+ session.roomId = void 0;
149
+ session.user = void 0;
150
+ return;
151
+ }
152
+ const leavingUserId = room.usersByConnection.get(session.id)?.userId;
153
+ room.connections.delete(session.id);
154
+ room.usersByConnection.delete(session.id);
155
+ session.roomId = void 0;
156
+ session.user = void 0;
157
+ if (notify && leavingUserId) {
158
+ this.broadcastToRoom(roomId, {
159
+ type: "room.user_left",
160
+ roomId,
161
+ userId: leavingUserId,
162
+ onlineCount: room.connections.size
163
+ });
164
+ }
165
+ if (room.connections.size === 0) {
166
+ this.rooms.delete(roomId);
167
+ }
168
+ }
169
+ handleDanmaku(session, payload) {
170
+ if (!session.roomId || !session.user) {
171
+ this.sendError(session.id, "NOT_JOINED", "Join a room before sending danmaku.");
172
+ return;
173
+ }
174
+ const room = this.rooms.get(session.roomId);
175
+ if (!room) {
176
+ this.sendError(session.id, "ROOM_NOT_FOUND", "Room does not exist.");
177
+ return;
178
+ }
179
+ const text = payload.text.trim().slice(0, this.maxDanmakuLength);
180
+ if (!text) {
181
+ this.sendError(session.id, "EMPTY_DANMAKU", "Danmaku text cannot be empty.");
182
+ return;
183
+ }
184
+ const event = {
185
+ id: `${session.roomId}-d-${room.seq++}`,
186
+ roomId: session.roomId,
187
+ text,
188
+ color: payload.color,
189
+ kind: payload.kind,
190
+ user: session.user,
191
+ timestamp: Date.now()
192
+ };
193
+ room.danmakuHistory.push(event);
194
+ trimHistory(room.danmakuHistory, this.historyLimit);
195
+ this.broadcastToRoom(session.roomId, {
196
+ type: "danmaku.broadcast",
197
+ roomId: session.roomId,
198
+ event
199
+ });
200
+ }
201
+ handleFirework(session, payload) {
202
+ if (!session.roomId || !session.user) {
203
+ this.sendError(session.id, "NOT_JOINED", "Join a room before launching fireworks.");
204
+ return;
205
+ }
206
+ const room = this.rooms.get(session.roomId);
207
+ if (!room) {
208
+ this.sendError(session.id, "ROOM_NOT_FOUND", "Room does not exist.");
209
+ return;
210
+ }
211
+ const event = {
212
+ id: `${session.roomId}-f-${room.seq++}`,
213
+ roomId: session.roomId,
214
+ payload,
215
+ user: session.user,
216
+ timestamp: Date.now()
217
+ };
218
+ room.fireworkHistory.push(event);
219
+ trimHistory(room.fireworkHistory, this.historyLimit);
220
+ this.broadcastToRoom(session.roomId, {
221
+ type: "firework.broadcast",
222
+ roomId: session.roomId,
223
+ event
224
+ });
225
+ }
226
+ createRoom(roomId) {
227
+ const room = {
228
+ id: roomId,
229
+ connections: /* @__PURE__ */ new Set(),
230
+ usersByConnection: /* @__PURE__ */ new Map(),
231
+ danmakuHistory: [],
232
+ fireworkHistory: [],
233
+ seq: 1
234
+ };
235
+ this.rooms.set(roomId, room);
236
+ return room;
237
+ }
238
+ sendToConnection(connectionId, message) {
239
+ const session = this.sessions.get(connectionId);
240
+ if (!session) {
241
+ return;
242
+ }
243
+ try {
244
+ session.transport.send(JSON.stringify(message));
245
+ } catch (error) {
246
+ this.logger?.warn?.("[FireworksRoomHub] failed to send message", error);
247
+ }
248
+ }
249
+ broadcastToRoom(roomId, message, excludeConnectionId) {
250
+ const room = this.rooms.get(roomId);
251
+ if (!room) {
252
+ return;
253
+ }
254
+ for (const connectionId of room.connections) {
255
+ if (excludeConnectionId && connectionId === excludeConnectionId) {
256
+ continue;
257
+ }
258
+ this.sendToConnection(connectionId, message);
259
+ }
260
+ }
261
+ sendError(connectionId, code, message) {
262
+ this.sendToConnection(connectionId, {
263
+ type: "error",
264
+ code,
265
+ message
266
+ });
267
+ }
268
+ };
269
+ function createId(prefix) {
270
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
271
+ }
272
+ function trimHistory(history, limit) {
273
+ if (history.length <= limit) {
274
+ return;
275
+ }
276
+ history.splice(0, history.length - limit);
277
+ }
278
+ function parseClientMessage(raw) {
279
+ if (typeof raw === "string") {
280
+ try {
281
+ const parsed = JSON.parse(raw);
282
+ return { ok: true, value: parsed };
283
+ } catch {
284
+ return { ok: false, error: "Invalid JSON payload." };
285
+ }
286
+ }
287
+ if (isObject(raw)) {
288
+ return { ok: true, value: raw };
289
+ }
290
+ return { ok: false, error: "Unsupported message payload." };
291
+ }
292
+ function isObject(value) {
293
+ return typeof value === "object" && value !== null;
294
+ }
295
+
296
+ // src/mikuFireworks3D/server/wsAdapter.ts
297
+ function bindWsSocket(hub, socket, options) {
298
+ const connection = hub.connect(
299
+ {
300
+ send: (encodedMessage) => {
301
+ socket.send(encodedMessage);
302
+ }
303
+ },
304
+ options?.connectionId
305
+ );
306
+ options?.onConnected?.(connection);
307
+ socket.on("message", (raw) => {
308
+ connection.handleMessage(normalizeWsRawData(raw));
309
+ });
310
+ socket.on("close", () => {
311
+ connection.disconnect("socket_closed");
312
+ options?.onDisconnected?.(connection.id);
313
+ });
314
+ socket.on("error", () => {
315
+ connection.disconnect("socket_error");
316
+ options?.onDisconnected?.(connection.id);
317
+ });
318
+ return connection;
319
+ }
320
+ function normalizeWsRawData(raw) {
321
+ if (typeof raw === "string") {
322
+ return raw;
323
+ }
324
+ if (raw instanceof ArrayBuffer) {
325
+ return new TextDecoder().decode(new Uint8Array(raw));
326
+ }
327
+ if (ArrayBuffer.isView(raw)) {
328
+ return new TextDecoder().decode(raw);
329
+ }
330
+ return "";
331
+ }
332
+
333
+ export { FireworksRoomHub, bindWsSocket };
334
+ //# sourceMappingURL=index.mjs.map
335
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/mikuFireworks3D/server/FireworksRoomHub.ts","../../../src/mikuFireworks3D/server/wsAdapter.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type {\n DanmakuBroadcastEvent,\n FireworkBroadcastEvent,\n FireworksClientMessage,\n FireworksConnection,\n FireworksConnectionTransport,\n FireworksRoomHubOptions,\n FireworksServerMessage,\n FireworksUserIdentity,\n} from './types';\n\ninterface Session {\n id: string;\n transport: FireworksConnectionTransport;\n roomId?: string;\n user?: FireworksUserIdentity;\n}\n\ninterface RoomState {\n id: string;\n connections: Set<string>;\n usersByConnection: Map<string, FireworksUserIdentity>;\n danmakuHistory: DanmakuBroadcastEvent[];\n fireworkHistory: FireworkBroadcastEvent[];\n seq: number;\n}\n\nconst DEFAULT_MAX_USERS_PER_ROOM = 200;\nconst DEFAULT_HISTORY_LIMIT = 50;\nconst DEFAULT_MAX_DANMAKU_LENGTH = 64;\n\nexport class FireworksRoomHub {\n private readonly rooms = new Map<string, RoomState>();\n private readonly sessions = new Map<string, Session>();\n private readonly maxUsersPerRoom: number;\n private readonly historyLimit: number;\n private readonly maxDanmakuLength: number;\n private readonly logger?: Pick<Console, 'info' | 'warn' | 'error'>;\n\n constructor(options?: FireworksRoomHubOptions) {\n this.maxUsersPerRoom = options?.maxUsersPerRoom ?? DEFAULT_MAX_USERS_PER_ROOM;\n this.historyLimit = options?.historyLimit ?? DEFAULT_HISTORY_LIMIT;\n this.maxDanmakuLength = options?.maxDanmakuLength ?? DEFAULT_MAX_DANMAKU_LENGTH;\n this.logger = options?.logger;\n }\n\n connect(transport: FireworksConnectionTransport, id?: string): FireworksConnection {\n const connectionId = id ?? createId('conn');\n const session: Session = {\n id: connectionId,\n transport,\n };\n\n this.sessions.set(connectionId, session);\n\n return {\n id: connectionId,\n handleMessage: (raw) => this.handleRawMessage(connectionId, raw),\n disconnect: (reason) => this.disconnect(connectionId, reason),\n send: (message) => this.sendToConnection(connectionId, message),\n };\n }\n\n disconnect(connectionId: string, reason?: string): void {\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n this.leaveRoom(session, false);\n this.sessions.delete(connectionId);\n\n if (reason) {\n this.logger?.info?.(`[FireworksRoomHub] disconnected ${connectionId}: ${reason}`);\n }\n }\n\n getStats(): { rooms: number; connections: number } {\n return {\n rooms: this.rooms.size,\n connections: this.sessions.size,\n };\n }\n\n getRoomOnlineCount(roomId: string): number {\n return this.rooms.get(roomId)?.connections.size ?? 0;\n }\n\n private handleRawMessage(connectionId: string, raw: unknown): void {\n const message = parseClientMessage(raw);\n if (!message.ok) {\n this.sendError(connectionId, 'BAD_MESSAGE', message.error);\n return;\n }\n\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n const clientMessage = message.value;\n\n switch (clientMessage.type) {\n case 'join': {\n this.joinRoom(session, clientMessage.roomId, clientMessage.user);\n return;\n }\n case 'leave': {\n this.leaveRoom(session, true);\n return;\n }\n case 'danmaku.send': {\n this.handleDanmaku(session, clientMessage.payload);\n return;\n }\n case 'firework.launch': {\n this.handleFirework(session, clientMessage.payload);\n return;\n }\n case 'ping': {\n this.sendToConnection(connectionId, {\n type: 'pong',\n ts: clientMessage.ts ?? Date.now(),\n });\n return;\n }\n default: {\n this.sendError(connectionId, 'UNSUPPORTED_MESSAGE', 'Unsupported message type.');\n }\n }\n }\n\n private joinRoom(session: Session, roomId: string, user: FireworksUserIdentity): void {\n const normalizedRoomId = roomId.trim();\n const normalizedUserId = user.userId.trim();\n\n if (!normalizedRoomId) {\n this.sendError(session.id, 'ROOM_ID_REQUIRED', 'roomId is required.');\n return;\n }\n if (!normalizedUserId) {\n this.sendError(session.id, 'USER_ID_REQUIRED', 'user.userId is required.');\n return;\n }\n\n if (session.roomId && session.roomId !== normalizedRoomId) {\n this.leaveRoom(session, true);\n }\n\n const room = this.rooms.get(normalizedRoomId) ?? this.createRoom(normalizedRoomId);\n if (room.connections.size >= this.maxUsersPerRoom && !room.connections.has(session.id)) {\n this.sendError(session.id, 'ROOM_FULL', 'Room is full.');\n return;\n }\n\n const identity: FireworksUserIdentity = {\n userId: normalizedUserId,\n nickname: user.nickname,\n avatarUrl: user.avatarUrl,\n };\n\n const wasMember = room.connections.has(session.id);\n room.connections.add(session.id);\n room.usersByConnection.set(session.id, identity);\n\n session.roomId = normalizedRoomId;\n session.user = identity;\n\n this.sendToConnection(session.id, {\n type: 'joined',\n roomId: normalizedRoomId,\n self: identity,\n onlineCount: room.connections.size,\n });\n\n this.sendToConnection(session.id, {\n type: 'room.snapshot',\n roomId: normalizedRoomId,\n users: Array.from(room.usersByConnection.values()),\n danmakuHistory: [...room.danmakuHistory],\n fireworkHistory: [...room.fireworkHistory],\n });\n\n if (!wasMember) {\n this.broadcastToRoom(normalizedRoomId, {\n type: 'room.user_joined',\n roomId: normalizedRoomId,\n user: identity,\n onlineCount: room.connections.size,\n }, session.id);\n }\n }\n\n private leaveRoom(session: Session, notify: boolean): void {\n const roomId = session.roomId;\n if (!roomId) {\n return;\n }\n\n const room = this.rooms.get(roomId);\n if (!room) {\n session.roomId = undefined;\n session.user = undefined;\n return;\n }\n\n const leavingUserId = room.usersByConnection.get(session.id)?.userId;\n\n room.connections.delete(session.id);\n room.usersByConnection.delete(session.id);\n\n session.roomId = undefined;\n session.user = undefined;\n\n if (notify && leavingUserId) {\n this.broadcastToRoom(roomId, {\n type: 'room.user_left',\n roomId,\n userId: leavingUserId,\n onlineCount: room.connections.size,\n });\n }\n\n if (room.connections.size === 0) {\n this.rooms.delete(roomId);\n }\n }\n\n private handleDanmaku(\n session: Session,\n payload: {\n text: string;\n color?: string;\n kind?: 'normal' | 'miku' | 'avatar';\n }\n ): void {\n if (!session.roomId || !session.user) {\n this.sendError(session.id, 'NOT_JOINED', 'Join a room before sending danmaku.');\n return;\n }\n\n const room = this.rooms.get(session.roomId);\n if (!room) {\n this.sendError(session.id, 'ROOM_NOT_FOUND', 'Room does not exist.');\n return;\n }\n\n const text = payload.text.trim().slice(0, this.maxDanmakuLength);\n if (!text) {\n this.sendError(session.id, 'EMPTY_DANMAKU', 'Danmaku text cannot be empty.');\n return;\n }\n\n const event: DanmakuBroadcastEvent = {\n id: `${session.roomId}-d-${room.seq++}`,\n roomId: session.roomId,\n text,\n color: payload.color,\n kind: payload.kind,\n user: session.user,\n timestamp: Date.now(),\n };\n\n room.danmakuHistory.push(event);\n trimHistory(room.danmakuHistory, this.historyLimit);\n\n this.broadcastToRoom(session.roomId, {\n type: 'danmaku.broadcast',\n roomId: session.roomId,\n event,\n });\n }\n\n private handleFirework(session: Session, payload: FireworkBroadcastEvent['payload']): void {\n if (!session.roomId || !session.user) {\n this.sendError(session.id, 'NOT_JOINED', 'Join a room before launching fireworks.');\n return;\n }\n\n const room = this.rooms.get(session.roomId);\n if (!room) {\n this.sendError(session.id, 'ROOM_NOT_FOUND', 'Room does not exist.');\n return;\n }\n\n const event: FireworkBroadcastEvent = {\n id: `${session.roomId}-f-${room.seq++}`,\n roomId: session.roomId,\n payload,\n user: session.user,\n timestamp: Date.now(),\n };\n\n room.fireworkHistory.push(event);\n trimHistory(room.fireworkHistory, this.historyLimit);\n\n this.broadcastToRoom(session.roomId, {\n type: 'firework.broadcast',\n roomId: session.roomId,\n event,\n });\n }\n\n private createRoom(roomId: string): RoomState {\n const room: RoomState = {\n id: roomId,\n connections: new Set(),\n usersByConnection: new Map(),\n danmakuHistory: [],\n fireworkHistory: [],\n seq: 1,\n };\n\n this.rooms.set(roomId, room);\n return room;\n }\n\n private sendToConnection(connectionId: string, message: FireworksServerMessage): void {\n const session = this.sessions.get(connectionId);\n if (!session) {\n return;\n }\n\n try {\n session.transport.send(JSON.stringify(message));\n } catch (error) {\n this.logger?.warn?.('[FireworksRoomHub] failed to send message', error);\n }\n }\n\n private broadcastToRoom(roomId: string, message: FireworksServerMessage, excludeConnectionId?: string): void {\n const room = this.rooms.get(roomId);\n if (!room) {\n return;\n }\n\n for (const connectionId of room.connections) {\n if (excludeConnectionId && connectionId === excludeConnectionId) {\n continue;\n }\n this.sendToConnection(connectionId, message);\n }\n }\n\n private sendError(connectionId: string, code: string, message: string): void {\n this.sendToConnection(connectionId, {\n type: 'error',\n code,\n message,\n });\n }\n}\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction trimHistory<T>(history: T[], limit: number): void {\n if (history.length <= limit) {\n return;\n }\n history.splice(0, history.length - limit);\n}\n\nfunction parseClientMessage(raw: unknown):\n | { ok: true; value: FireworksClientMessage }\n | { ok: false; error: string } {\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw) as FireworksClientMessage;\n return { ok: true, value: parsed };\n } catch {\n return { ok: false, error: 'Invalid JSON payload.' };\n }\n }\n\n if (isObject(raw)) {\n return { ok: true, value: raw as FireworksClientMessage };\n }\n\n return { ok: false, error: 'Unsupported message payload.' };\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n","import type { FireworksRoomHub } from './FireworksRoomHub';\nimport type { FireworksConnection, WsLikeSocket } from './types';\n\nexport interface BindWsSocketOptions {\n connectionId?: string;\n onConnected?: (connection: FireworksConnection) => void;\n onDisconnected?: (connectionId: string) => void;\n}\n\n/**\n * Bind a ws-like socket to FireworksRoomHub.\n *\n * Example (ws package):\n * const hub = new FireworksRoomHub();\n * wss.on('connection', (socket) => bindWsSocket(hub, socket));\n */\nexport function bindWsSocket(\n hub: FireworksRoomHub,\n socket: WsLikeSocket,\n options?: BindWsSocketOptions\n): FireworksConnection {\n const connection = hub.connect(\n {\n send: (encodedMessage) => {\n socket.send(encodedMessage);\n },\n },\n options?.connectionId\n );\n\n options?.onConnected?.(connection);\n\n socket.on('message', (raw) => {\n connection.handleMessage(normalizeWsRawData(raw));\n });\n\n socket.on('close', () => {\n connection.disconnect('socket_closed');\n options?.onDisconnected?.(connection.id);\n });\n\n socket.on('error', () => {\n connection.disconnect('socket_error');\n options?.onDisconnected?.(connection.id);\n });\n\n return connection;\n}\n\nfunction normalizeWsRawData(raw: unknown): string {\n if (typeof raw === 'string') {\n return raw;\n }\n\n if (raw instanceof ArrayBuffer) {\n return new TextDecoder().decode(new Uint8Array(raw));\n }\n\n if (ArrayBuffer.isView(raw)) {\n return new TextDecoder().decode(raw);\n }\n\n return '';\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import React__default, { RefObject } from 'react';
2
2
 
3
3
  type MikuFusionGameStatus = 'ready' | 'playing' | 'paused' | 'gameOver';
4
+ type OrbImageMapping = Partial<Record<number, string>>;
4
5
  interface FusionOrb {
5
6
  id: string;
6
7
  x: number;
@@ -46,11 +47,15 @@ interface MergeResult {
46
47
  interface MikuFusionGameProps extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
47
48
  className?: string;
48
49
  storageKey?: string;
50
+ orbImageStorageKey?: string;
51
+ enableImageConfigPanel?: boolean;
52
+ presetOrbImageMapping?: OrbImageMapping;
49
53
  }
50
- declare function MikuFusionGame({ className, storageKey, ...options }: MikuFusionGameProps): React__default.JSX.Element;
54
+ declare function MikuFusionGame({ className, storageKey, orbImageStorageKey, enableImageConfigPanel, presetOrbImageMapping, ...options }: MikuFusionGameProps): React__default.JSX.Element;
51
55
 
52
56
  interface UseMikuFusionGameOptions extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
53
57
  storageKey?: string;
58
+ orbImageMapping?: OrbImageMapping;
54
59
  }
55
60
  interface UseMikuFusionGameResult {
56
61
  canvasRef: RefObject<HTMLCanvasElement>;
@@ -109,4 +114,4 @@ declare const RADIUS_STEP = 4;
109
114
  declare const LEVEL_LABEL_PREFIX = "M";
110
115
  declare const DEFAULT_MIKU_FUSION_CONFIG: MikuFusionGameConfig;
111
116
 
112
- export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
117
+ export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type OrbImageMapping, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
@@ -1,6 +1,7 @@
1
1
  import React__default, { RefObject } from 'react';
2
2
 
3
3
  type MikuFusionGameStatus = 'ready' | 'playing' | 'paused' | 'gameOver';
4
+ type OrbImageMapping = Partial<Record<number, string>>;
4
5
  interface FusionOrb {
5
6
  id: string;
6
7
  x: number;
@@ -46,11 +47,15 @@ interface MergeResult {
46
47
  interface MikuFusionGameProps extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
47
48
  className?: string;
48
49
  storageKey?: string;
50
+ orbImageStorageKey?: string;
51
+ enableImageConfigPanel?: boolean;
52
+ presetOrbImageMapping?: OrbImageMapping;
49
53
  }
50
- declare function MikuFusionGame({ className, storageKey, ...options }: MikuFusionGameProps): React__default.JSX.Element;
54
+ declare function MikuFusionGame({ className, storageKey, orbImageStorageKey, enableImageConfigPanel, presetOrbImageMapping, ...options }: MikuFusionGameProps): React__default.JSX.Element;
51
55
 
52
56
  interface UseMikuFusionGameOptions extends Partial<MikuFusionGameConfig>, MikuFusionGameCallbacks {
53
57
  storageKey?: string;
58
+ orbImageMapping?: OrbImageMapping;
54
59
  }
55
60
  interface UseMikuFusionGameResult {
56
61
  canvasRef: RefObject<HTMLCanvasElement>;
@@ -109,4 +114,4 @@ declare const RADIUS_STEP = 4;
109
114
  declare const LEVEL_LABEL_PREFIX = "M";
110
115
  declare const DEFAULT_MIKU_FUSION_CONFIG: MikuFusionGameConfig;
111
116
 
112
- export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
117
+ export { BASE_RADIUS, DEFAULT_MIKU_FUSION_CONFIG, type FusionOrb, LEVEL_LABEL_PREFIX, type MergeResult, type MikuFusionContentRegistry, MikuFusionGame, type MikuFusionGameCallbacks, type MikuFusionGameConfig, type MikuFusionGameProps, type MikuFusionGameStatus, type MikuFusionTheme, type OrbImageMapping, type ProducerPack, RADIUS_STEP, type SongPack, type ThemePack, type UseMikuFusionGameOptions, type UseMikuFusionGameResult, defaultContentRegistry, useMikuFusionGame, useResponsiveCanvas };
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ var chunkHHVDOIPV_js = require('../chunk-HHVDOIPV.js');
4
+ require('../chunk-ZWQJSZEY.js');
3
5
  var chunkKO73EBUT_js = require('../chunk-KO73EBUT.js');
4
6
  require('../chunk-XJ7ZAGC5.js');
7
+ require('../chunk-25OFOKNF.js');
5
8
  require('../chunk-DGUM43GV.js');
6
9
  var React3 = require('react');
7
10
 
@@ -223,7 +226,7 @@ function getOrbColor(level, config) {
223
226
  const index = Math.max(0, Math.min(config.theme.orbColors.length - 1, level - 1));
224
227
  return config.theme.orbColors[index] || "#22d3ee";
225
228
  }
226
- function drawScene(context, width, height, config, orbs, aimX, nextLevel, status) {
229
+ function drawScene(context, width, height, config, orbs, aimX, nextLevel, status, imageCache) {
227
230
  const gradient = context.createLinearGradient(0, 0, 0, height);
228
231
  gradient.addColorStop(0, config.theme.backgroundTop);
229
232
  gradient.addColorStop(1, config.theme.backgroundBottom);
@@ -244,15 +247,51 @@ function drawScene(context, width, height, config, orbs, aimX, nextLevel, status
244
247
  context.lineTo(aimX, config.spawnY);
245
248
  context.stroke();
246
249
  const previewRadius = getRadiusByLevel(nextLevel);
247
- context.fillStyle = getOrbColor(nextLevel, config);
250
+ const previewY = config.spawnY - previewRadius - 4;
251
+ const previewImage = imageCache[nextLevel];
248
252
  context.beginPath();
249
- context.arc(aimX, config.spawnY - previewRadius - 4, previewRadius, 0, Math.PI * 2);
250
- context.fill();
253
+ context.arc(aimX, previewY, previewRadius, 0, Math.PI * 2);
254
+ context.closePath();
255
+ if (previewImage && previewImage.complete && previewImage.naturalWidth > 0) {
256
+ context.save();
257
+ context.clip();
258
+ context.drawImage(
259
+ previewImage,
260
+ aimX - previewRadius,
261
+ previewY - previewRadius,
262
+ previewRadius * 2,
263
+ previewRadius * 2
264
+ );
265
+ context.restore();
266
+ } else {
267
+ context.fillStyle = getOrbColor(nextLevel, config);
268
+ context.fill();
269
+ }
251
270
  orbs.forEach((orb) => {
252
271
  context.beginPath();
253
- context.fillStyle = getOrbColor(orb.level, config);
254
272
  context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
255
- context.fill();
273
+ context.closePath();
274
+ const orbImage = imageCache[orb.level];
275
+ if (orbImage && orbImage.complete && orbImage.naturalWidth > 0) {
276
+ context.save();
277
+ context.clip();
278
+ context.drawImage(
279
+ orbImage,
280
+ orb.x - orb.radius,
281
+ orb.y - orb.radius,
282
+ orb.radius * 2,
283
+ orb.radius * 2
284
+ );
285
+ context.restore();
286
+ context.beginPath();
287
+ context.strokeStyle = "rgba(15, 23, 42, 0.15)";
288
+ context.lineWidth = 1;
289
+ context.arc(orb.x, orb.y, orb.radius, 0, Math.PI * 2);
290
+ context.stroke();
291
+ } else {
292
+ context.fillStyle = getOrbColor(orb.level, config);
293
+ context.fill();
294
+ }
256
295
  context.fillStyle = "#0f172a";
257
296
  context.font = "bold 12px system-ui";
258
297
  context.textAlign = "center";
@@ -291,10 +330,26 @@ function useMikuFusionGame(options = {}) {
291
330
  const config = configRef.current;
292
331
  const onScoreChangeRef = React3.useRef(options.onScoreChange);
293
332
  const onGameOverRef = React3.useRef(options.onGameOver);
333
+ const orbImageMappingRef = React3.useRef(options.orbImageMapping ?? {});
334
+ const imageCacheRef = React3.useRef({});
294
335
  React3.useEffect(() => {
295
336
  onScoreChangeRef.current = options.onScoreChange;
296
337
  onGameOverRef.current = options.onGameOver;
297
338
  }, [options.onGameOver, options.onScoreChange]);
339
+ React3.useEffect(() => {
340
+ orbImageMappingRef.current = options.orbImageMapping ?? {};
341
+ const nextCache = {};
342
+ Object.entries(orbImageMappingRef.current).forEach(([levelText, src]) => {
343
+ const level = Number(levelText);
344
+ if (!src || Number.isNaN(level)) {
345
+ return;
346
+ }
347
+ const image = new window.Image();
348
+ image.src = src;
349
+ nextCache[level] = image;
350
+ });
351
+ imageCacheRef.current = nextCache;
352
+ }, [options.orbImageMapping]);
298
353
  const [bestScore, setBestScore] = chunkKO73EBUT_js.useLocalStorage(
299
354
  options.storageKey || "sa2kit:mikuFusionGame:bestScore",
300
355
  0
@@ -327,7 +382,8 @@ function useMikuFusionGame(options = {}) {
327
382
  orbsRef.current,
328
383
  aimXRef.current,
329
384
  nextLevel,
330
- nextStatus
385
+ nextStatus,
386
+ imageCacheRef.current
331
387
  );
332
388
  },
333
389
  [config, nextLevel, status]
@@ -571,8 +627,24 @@ function GameResultModal({ open, score, bestScore, onRestart }) {
571
627
  }
572
628
 
573
629
  // src/mikuFusionGame/components/MikuFusionGame.tsx
574
- function MikuFusionGame({ className, storageKey, ...options }) {
630
+ function MikuFusionGame({
631
+ className,
632
+ storageKey,
633
+ orbImageStorageKey = "sa2kit:mikuFusionGame:orbImages",
634
+ enableImageConfigPanel = true,
635
+ presetOrbImageMapping = {},
636
+ ...options
637
+ }) {
575
638
  const containerRef = React3.useRef(null);
639
+ const [orbImageMapping, setOrbImageMapping] = React3.useState({});
640
+ const [isUserDefineMode] = React3.useState(() => {
641
+ if (typeof window === "undefined") {
642
+ return false;
643
+ }
644
+ const value = new URLSearchParams(window.location.search).get("userDefine");
645
+ return value === "true";
646
+ });
647
+ const activeOrbImageMapping = isUserDefineMode ? orbImageMapping : presetOrbImageMapping;
576
648
  const {
577
649
  canvasRef,
578
650
  status,
@@ -584,7 +656,46 @@ function MikuFusionGame({ className, storageKey, ...options }) {
584
656
  handleDrop,
585
657
  togglePause,
586
658
  restart
587
- } = useMikuFusionGame({ ...options, storageKey });
659
+ } = useMikuFusionGame({ ...options, storageKey, orbImageMapping: activeOrbImageMapping });
660
+ const panelItems = React3.useMemo(
661
+ () => Array.from({ length: config.maxLevel }, (_, index) => ({
662
+ id: String(index + 1),
663
+ label: `M${index + 1}`
664
+ })),
665
+ [config.maxLevel]
666
+ );
667
+ const panelValue = React3.useMemo(
668
+ () => Object.entries(orbImageMapping).reduce((acc, [key, value]) => {
669
+ if (value) {
670
+ acc[String(key)] = value;
671
+ }
672
+ return acc;
673
+ }, {}),
674
+ [orbImageMapping]
675
+ );
676
+ React3.useEffect(() => {
677
+ if (typeof window === "undefined") {
678
+ return;
679
+ }
680
+ try {
681
+ const raw = window.localStorage.getItem(orbImageStorageKey);
682
+ if (!raw) {
683
+ setOrbImageMapping({});
684
+ return;
685
+ }
686
+ const parsed = JSON.parse(raw);
687
+ const normalized = {};
688
+ Object.entries(parsed).forEach(([key, value]) => {
689
+ const level = Number(key);
690
+ if (!Number.isNaN(level) && typeof value === "string" && value) {
691
+ normalized[level] = value;
692
+ }
693
+ });
694
+ setOrbImageMapping(normalized);
695
+ } catch {
696
+ setOrbImageMapping({});
697
+ }
698
+ }, [orbImageStorageKey]);
588
699
  const { displayWidth, displayHeight } = useResponsiveCanvas(
589
700
  config.width,
590
701
  config.height,
@@ -624,6 +735,16 @@ function MikuFusionGame({ className, storageKey, ...options }) {
624
735
  togglePause();
625
736
  }
626
737
  };
738
+ const handlePanelValueChange = (next) => {
739
+ const normalized = {};
740
+ Object.entries(next).forEach(([key, value]) => {
741
+ const level = Number(key);
742
+ if (!Number.isNaN(level) && value) {
743
+ normalized[level] = value;
744
+ }
745
+ });
746
+ setOrbImageMapping(normalized);
747
+ };
627
748
  return /* @__PURE__ */ React3__default.default.createElement(
628
749
  "div",
629
750
  {
@@ -657,7 +778,17 @@ function MikuFusionGame({ className, storageKey, ...options }) {
657
778
  onTogglePause: togglePause,
658
779
  onRestart: restart
659
780
  }
660
- )
781
+ ),
782
+ enableImageConfigPanel && isUserDefineMode ? /* @__PURE__ */ React3__default.default.createElement(
783
+ chunkHHVDOIPV_js.LocalImageMappingPanel,
784
+ {
785
+ storageKey: orbImageStorageKey,
786
+ title: "\u7403\u4F53\u56FE\u7247\u914D\u7F6E\uFF08\u4FDD\u5B58\u5230\u672C\u5730\uFF09",
787
+ items: panelItems,
788
+ defaultValue: panelValue,
789
+ onValueChange: handlePanelValueChange
790
+ }
791
+ ) : null
661
792
  );
662
793
  }
663
794