vantiv.io 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +864 -0
- package/index.js +13 -0
- package/package.json +28 -0
- package/src/classes/Actions/Awaiter.js +202 -0
- package/src/classes/Actions/Channel.js +73 -0
- package/src/classes/Actions/Direct.js +263 -0
- package/src/classes/Actions/Inventory.js +156 -0
- package/src/classes/Actions/Music.js +278 -0
- package/src/classes/Actions/Player.js +377 -0
- package/src/classes/Actions/Public.js +66 -0
- package/src/classes/Actions/Room.js +333 -0
- package/src/classes/Actions/Utils.js +29 -0
- package/src/classes/Actions/lib/AudioStreaming.js +447 -0
- package/src/classes/Caches/MovementCache.js +357 -0
- package/src/classes/Handlers/AxiosErrorHandler.js +68 -0
- package/src/classes/Handlers/ErrorHandler.js +65 -0
- package/src/classes/Handlers/EventHandlers.js +259 -0
- package/src/classes/Handlers/WebSocketHandlers.js +54 -0
- package/src/classes/Managers/ChannelManager.js +303 -0
- package/src/classes/Managers/DanceFloorManagers.js +509 -0
- package/src/classes/Managers/Helpers/CleanupManager.js +130 -0
- package/src/classes/Managers/Helpers/LoggerManager.js +171 -0
- package/src/classes/Managers/Helpers/MetricsManager.js +83 -0
- package/src/classes/Managers/Networking/ConnectionManager.js +259 -0
- package/src/classes/Managers/Networking/CooldownManager.js +516 -0
- package/src/classes/Managers/Networking/EventsManager.js +64 -0
- package/src/classes/Managers/Networking/KeepAliveManager.js +109 -0
- package/src/classes/Managers/Networking/MessageHandler.js +110 -0
- package/src/classes/Managers/Networking/Request.js +329 -0
- package/src/classes/Managers/PermissionManager.js +288 -0
- package/src/classes/WebApi/Category/Grab.js +98 -0
- package/src/classes/WebApi/Category/Item.js +347 -0
- package/src/classes/WebApi/Category/Post.js +154 -0
- package/src/classes/WebApi/Category/Room.js +137 -0
- package/src/classes/WebApi/Category/User.js +88 -0
- package/src/classes/WebApi/webapi.js +52 -0
- package/src/constants/TypesConstants.js +89 -0
- package/src/constants/WebSocketConstants.js +80 -0
- package/src/core/Highrise.js +123 -0
- package/src/core/HighriseWebsocket.js +228 -0
- package/src/utils/ConvertSvgToPng.js +51 -0
- package/src/utils/ModelPool.js +160 -0
- package/src/utils/Models.js +128 -0
- package/src/utils/versionCheck.js +27 -0
- package/src/validators/ConfigValidator.js +205 -0
- package/src/validators/ConnectionValidator.js +65 -0
- package/typings/index.d.ts +3820 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class DanceFloor {
|
|
5
|
+
constructor(bot) {
|
|
6
|
+
this.bot = bot
|
|
7
|
+
this.DanceFloorIds = new Map()
|
|
8
|
+
this.UserToFloorDance = new Map()
|
|
9
|
+
this.fileWatchers = new Map()
|
|
10
|
+
this.originalEmotes = new Map()
|
|
11
|
+
|
|
12
|
+
this.bot.on('Movement', async (user, position, anchor) => {
|
|
13
|
+
await this._handlePlayerMovement(user.id, user.username, position, anchor);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
this.bot.on('UserLeft', (user) => {
|
|
17
|
+
this._removePlayer(user.id);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async create(Id, corners, emotes) {
|
|
22
|
+
this._validateId(Id)
|
|
23
|
+
this._checkDuplicateId(Id)
|
|
24
|
+
this._validateCorners(corners)
|
|
25
|
+
|
|
26
|
+
const { emoteArray, filePath } = await this._parseEmotes(Id, emotes)
|
|
27
|
+
|
|
28
|
+
const danceFloor = {
|
|
29
|
+
id: Id,
|
|
30
|
+
corners: { ...corners },
|
|
31
|
+
emotes: emoteArray,
|
|
32
|
+
players: [],
|
|
33
|
+
filePath: filePath
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.DanceFloorIds.set(Id, danceFloor)
|
|
37
|
+
|
|
38
|
+
if (filePath) {
|
|
39
|
+
this._setupFileWatcher(Id, filePath)
|
|
40
|
+
this.originalEmotes.set(Id, emotes)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return danceFloor
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async hotReload(floorId) {
|
|
47
|
+
const floor = this.DanceFloorIds.get(floorId);
|
|
48
|
+
if (!floor) {
|
|
49
|
+
throw new Error(`Dance floor ${floorId} not found`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!floor.filePath) {
|
|
53
|
+
throw new Error(`No file path for hot-reload`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const { emoteArray } = await this._parseEmotes(floorId, floor.filePath, true);
|
|
58
|
+
|
|
59
|
+
floor.emotes = emoteArray;
|
|
60
|
+
|
|
61
|
+
for (const player of floor.players) {
|
|
62
|
+
player.emoteIndex = player.emoteIndex % emoteArray.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`Hot-reload failed: ${error.message}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async reloadAll() {
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const [floorId, floor] of this.DanceFloorIds) {
|
|
76
|
+
if (floor.filePath) {
|
|
77
|
+
try {
|
|
78
|
+
const success = await this.hotReload(floorId);
|
|
79
|
+
results.push({ floorId, success });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
results.push({ floorId, success: false, error: error.message });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async updateEmotes(floorId, newEmotes) {
|
|
89
|
+
const floor = this.DanceFloorIds.get(floorId);
|
|
90
|
+
if (!floor) {
|
|
91
|
+
throw new Error(`Dance floor ${floorId} not found`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { emoteArray } = await this._parseEmotes(floorId, newEmotes);
|
|
95
|
+
|
|
96
|
+
floor.emotes = emoteArray;
|
|
97
|
+
floor.filePath = null;
|
|
98
|
+
|
|
99
|
+
this.originalEmotes.set(floorId, newEmotes);
|
|
100
|
+
|
|
101
|
+
if (this.fileWatchers.has(floorId)) {
|
|
102
|
+
this.fileWatchers.get(floorId).close();
|
|
103
|
+
this.fileWatchers.delete(floorId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const player of floor.players) {
|
|
107
|
+
player.emoteIndex = player.emoteIndex % emoteArray.length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
destroy() {
|
|
114
|
+
for (const [floorId, floor] of this.DanceFloorIds) {
|
|
115
|
+
for (const player of floor.players) {
|
|
116
|
+
this._stopDancing(player.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const watcher of this.fileWatchers.values()) {
|
|
121
|
+
watcher.close();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.DanceFloorIds.clear();
|
|
125
|
+
this.UserToFloorDance.clear();
|
|
126
|
+
this.fileWatchers.clear();
|
|
127
|
+
this.originalEmotes.clear();
|
|
128
|
+
|
|
129
|
+
this.bot.removeAllListeners('Movement');
|
|
130
|
+
this.bot.removeAllListeners('UserLeft');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getInfo(Id) {
|
|
134
|
+
if (!this.DanceFloorIds.has(Id)) return null
|
|
135
|
+
const floor = this.DanceFloorIds.get(Id)
|
|
136
|
+
return {
|
|
137
|
+
id: floor.id,
|
|
138
|
+
corners: floor.corners,
|
|
139
|
+
emotes: floor.emotes,
|
|
140
|
+
playerCount: floor.players,
|
|
141
|
+
filePath: floor.filePath,
|
|
142
|
+
hasWatcher: this.fileWatchers.has(Id)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_setupFileWatcher(floorId, filePath) {
|
|
147
|
+
if (this.fileWatchers.has(floorId)) {
|
|
148
|
+
this.fileWatchers.get(floorId).close();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const watcher = fs.watch(filePath, { persistent: false }, (eventType) => {
|
|
153
|
+
if (eventType === 'change') {
|
|
154
|
+
setTimeout(() => {
|
|
155
|
+
this.hotReload(floorId).catch(err => {
|
|
156
|
+
console.error(`Auto-reload failed for ${floorId}: ${err.message}`);
|
|
157
|
+
});
|
|
158
|
+
}, 100);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
watcher.on('error', (err) => {
|
|
163
|
+
console.error(`File watcher error for ${floorId}: ${err.message}`);
|
|
164
|
+
this.fileWatchers.delete(floorId);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.fileWatchers.set(floorId, watcher);
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.warn(`Could not setup file watcher for ${floorId}: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async _parseEmotes(floorId, File, isHotReload = false) {
|
|
175
|
+
let filePath = null;
|
|
176
|
+
let emoteArray;
|
|
177
|
+
|
|
178
|
+
if (typeof File === 'string') {
|
|
179
|
+
filePath = path.resolve(File);
|
|
180
|
+
emoteArray = await this._validateAndParseEmotes(filePath);
|
|
181
|
+
|
|
182
|
+
} else if (Array.isArray(File)) {
|
|
183
|
+
emoteArray = this._parseEmoteArray(File);
|
|
184
|
+
|
|
185
|
+
} else if (typeof File === 'object' && File !== null) {
|
|
186
|
+
emoteArray = this._parseSingleEmote(File);
|
|
187
|
+
|
|
188
|
+
} else {
|
|
189
|
+
throw new TypeError('Invalid emotes format');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!isHotReload && filePath) {
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
console.log(`📁 Loaded ${emoteArray.length} emotes from: ${filePath}`);
|
|
195
|
+
}, 300);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { emoteArray, filePath };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async _addPlayer(floorId, userId, username, position) {
|
|
202
|
+
const floor = this.DanceFloorIds.get(floorId);
|
|
203
|
+
if (!floor) throw new Error(`Dance floor ${floorId} not found`);
|
|
204
|
+
|
|
205
|
+
if (!this._isInDanceFloor(position, floor.corners)) {
|
|
206
|
+
throw new Error(`Player ${username} not in boundaries`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const newPlayer = {
|
|
210
|
+
id: userId,
|
|
211
|
+
username,
|
|
212
|
+
position,
|
|
213
|
+
timeoutId: null,
|
|
214
|
+
emoteIndex: 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
floor.players.push(newPlayer);
|
|
218
|
+
this.UserToFloorDance.set(userId, floorId);
|
|
219
|
+
|
|
220
|
+
await this._startDancing(userId, floor);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_removePlayer(userId) {
|
|
224
|
+
const floorId = this.UserToFloorDance.get(userId);
|
|
225
|
+
if (!floorId) return;
|
|
226
|
+
|
|
227
|
+
const floor = this.DanceFloorIds.get(floorId);
|
|
228
|
+
floor.players = floor.players.filter(p => p.id !== userId);
|
|
229
|
+
this.UserToFloorDance.delete(userId);
|
|
230
|
+
|
|
231
|
+
this._stopDancing(userId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_getFloorByUser(userId) {
|
|
235
|
+
if (!this.UserToFloorDance.has(userId)) return null
|
|
236
|
+
return this.DanceFloorIds.get(this.UserToFloorDance.get(userId))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_isInDanceFloor(position, corners) {
|
|
240
|
+
const { x, y, z } = position;
|
|
241
|
+
const { x: minX, x1: maxX, y: minY, y1: maxY, z: minZ, z1: maxZ } = corners;
|
|
242
|
+
|
|
243
|
+
return x >= minX && x <= maxX &&
|
|
244
|
+
y >= minY && y <= maxY &&
|
|
245
|
+
z >= minZ && z <= maxZ;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async _handlePlayerMovement(userId, username, position, anchor) {
|
|
249
|
+
const currentFloorId = this.UserToFloorDance.get(userId);
|
|
250
|
+
|
|
251
|
+
for (const [floorId, floor] of this.DanceFloorIds) {
|
|
252
|
+
const isNowIn = anchor ? false : this._isInDanceFloor(position, floor.corners);
|
|
253
|
+
|
|
254
|
+
if (!currentFloorId && isNowIn) {
|
|
255
|
+
await this._addPlayer(floorId, userId, username, position);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
else if (currentFloorId === floorId && !isNowIn) {
|
|
260
|
+
this._removePlayer(userId);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
else if (currentFloorId === floorId && isNowIn) {
|
|
265
|
+
this._updatePlayerPosition(userId, position);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_updatePlayerPosition(userId, position) {
|
|
272
|
+
if (userId && typeof userId !== 'string') throw new TypeError('userId must be an string')
|
|
273
|
+
|
|
274
|
+
if (position && typeof position !== 'object') throw new TypeError('position must be an object')
|
|
275
|
+
if (!position.x || (position.x && typeof position.x !== 'number')) throw new TypeError('position.x must be number')
|
|
276
|
+
if (!position.y || (position.y && typeof position.y !== 'number')) throw new TypeError('position.y must be number')
|
|
277
|
+
if (!position.z || (position.z && typeof position.z !== 'number')) throw new TypeError('position.z must be number')
|
|
278
|
+
|
|
279
|
+
const floor = this._getFloorByUser(userId);
|
|
280
|
+
if (!floor) return;
|
|
281
|
+
|
|
282
|
+
const playerIndex = floor.players.findIndex(p => p.id === userId);
|
|
283
|
+
if (playerIndex !== -1) {
|
|
284
|
+
floor.players[playerIndex].position = position;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async _startDancing(userId, floor) {
|
|
290
|
+
const player = floor.players.find(p => p.id === userId);
|
|
291
|
+
if (!player) return;
|
|
292
|
+
|
|
293
|
+
const emoteLoop = async () => {
|
|
294
|
+
if (!this.UserToFloorDance.has(userId) ||
|
|
295
|
+
this.UserToFloorDance.get(userId) !== floor.id) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const currentEmote = floor.emotes[player.emoteIndex];
|
|
300
|
+
if (currentEmote?.id) {
|
|
301
|
+
const status = await this.bot.player.emote(currentEmote.id, userId)
|
|
302
|
+
if (!status) {
|
|
303
|
+
this._stopDancing(userId)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
player.emoteIndex = (player.emoteIndex + 1) % floor.emotes.length;
|
|
308
|
+
|
|
309
|
+
player.timeoutId = setTimeout(async () => {
|
|
310
|
+
player.timeoutId = null;
|
|
311
|
+
await emoteLoop();
|
|
312
|
+
}, currentEmote?.duration * 1000 || 3000);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (player.timeoutId) {
|
|
316
|
+
clearTimeout(player.timeoutId);
|
|
317
|
+
player.timeoutId = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await emoteLoop();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
_stopDancing(userId) {
|
|
324
|
+
const floorId = this.UserToFloorDance.get(userId);
|
|
325
|
+
if (!floorId) return false;
|
|
326
|
+
|
|
327
|
+
const floor = this.DanceFloorIds.get(floorId);
|
|
328
|
+
if (!floor) {
|
|
329
|
+
this.UserToFloorDance.delete(userId);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const playerIndex = floor.players.findIndex(p => p.id === userId);
|
|
334
|
+
if (playerIndex === -1) {
|
|
335
|
+
this.UserToFloorDance.delete(userId);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const player = floor.players[playerIndex];
|
|
340
|
+
|
|
341
|
+
if (player.timeoutId) {
|
|
342
|
+
clearTimeout(player.timeoutId);
|
|
343
|
+
player.timeoutId = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
this.bot.player.emote('idle', userId);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
floor.players.splice(playerIndex, 1);
|
|
352
|
+
|
|
353
|
+
this.UserToFloorDance.delete(userId);
|
|
354
|
+
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_validateId(Id) {
|
|
359
|
+
if (!Id || (Id && typeof Id !== 'string')) {
|
|
360
|
+
throw new TypeError('Id must be a non-empty string')
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_checkDuplicateId(Id) {
|
|
365
|
+
if (this.DanceFloorIds.has(Id)) {
|
|
366
|
+
throw new Error(`Dance floor with Id '${Id}' already exists`)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_validateCorners(corners) {
|
|
371
|
+
if (!corners || typeof corners !== 'object') {
|
|
372
|
+
throw new TypeError('corners must be an object with x, y, z, x1, y1, z1 properties')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const requiredCorners = ['x', 'y', 'z', 'x1', 'y1', 'z1']
|
|
376
|
+
for (const corner of requiredCorners) {
|
|
377
|
+
this._validateCornerValue(corners, corner)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_validateCornerValue(corners, cornerName) {
|
|
382
|
+
if (typeof corners[cornerName] !== 'number') {
|
|
383
|
+
throw new TypeError(`corner.${cornerName} must be a number`)
|
|
384
|
+
}
|
|
385
|
+
if (corners[cornerName] < 0 || corners[cornerName] > 30) {
|
|
386
|
+
throw new RangeError(`corner.${cornerName} must be between 0 and 30 (inclusive)`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async _validateAndParseEmotes(emotes) {
|
|
391
|
+
if (typeof emotes !== 'string') {
|
|
392
|
+
throw new TypeError('emotes must be a string file path to a JSON file')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const isFilePath = this._isValidFilePath(emotes)
|
|
396
|
+
if (!isFilePath) {
|
|
397
|
+
throw new TypeError('emotes must be a valid file path to a JSON file')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const jsonData = await this._readAndParseJsonFile(emotes)
|
|
401
|
+
this._validateJsonStructure(jsonData)
|
|
402
|
+
|
|
403
|
+
const emoteArray = this._parseEmotesFromJson(jsonData)
|
|
404
|
+
|
|
405
|
+
if (emoteArray.length === 0) {
|
|
406
|
+
throw new Error('JSON file must contain at least one emote in the emotes array')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return emoteArray
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_parseEmoteArray(emotes) {
|
|
413
|
+
if (!Array.isArray(emotes)) {
|
|
414
|
+
throw new TypeError('Emote array must be an array')
|
|
415
|
+
}
|
|
416
|
+
if (emotes.length === 0) {
|
|
417
|
+
throw new Error('Emote array must contain at least one emote')
|
|
418
|
+
}
|
|
419
|
+
return emotes.map((emote, index) => this._validateAndParseEmote(emote, index))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
_parseSingleEmote(emote) {
|
|
423
|
+
if (!emote || typeof emote !== 'object') {
|
|
424
|
+
throw new TypeError('Single emote must be an object')
|
|
425
|
+
}
|
|
426
|
+
return [this._validateAndParseEmote(emote, 0)]
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
_isValidFilePath(filePath) {
|
|
430
|
+
return filePath.endsWith('.json') ||
|
|
431
|
+
filePath.includes('/') ||
|
|
432
|
+
filePath.includes('\\') ||
|
|
433
|
+
filePath.includes('.json');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async _readAndParseJsonFile(filePath) {
|
|
437
|
+
try {
|
|
438
|
+
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
439
|
+
return JSON.parse(fileContent)
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this._handleFileError(error, filePath)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_handleFileError(error, filePath) {
|
|
446
|
+
if (error.code === 'ENOENT') {
|
|
447
|
+
throw new Error(`Emote file not found: ${filePath}`)
|
|
448
|
+
}
|
|
449
|
+
if (error instanceof SyntaxError) {
|
|
450
|
+
throw new Error(`Invalid JSON in file: ${filePath}`)
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`Failed to process emote file: ${error.message}`)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_validateJsonStructure(jsonData) {
|
|
456
|
+
if (!jsonData || typeof jsonData !== 'object') {
|
|
457
|
+
throw new Error('JSON file must contain an object')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!Array.isArray(jsonData.emotes)) {
|
|
461
|
+
throw new Error('JSON must have an "emotes" property containing an array')
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
_parseEmotesFromJson(jsonData) {
|
|
466
|
+
return jsonData.emotes.map((emote, index) => {
|
|
467
|
+
return this._validateAndParseEmote(emote, index)
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_validateAndParseEmote(emote, index) {
|
|
472
|
+
if (!emote || typeof emote !== 'object') {
|
|
473
|
+
throw new Error(`Emote at index ${index} must be an object`)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
this._validateEmoteProperty(emote, 'id', 'string', index)
|
|
477
|
+
this._validateEmoteProperty(emote, 'name', 'string', index)
|
|
478
|
+
this._validateEmoteProperty(emote, 'duration', 'number', index)
|
|
479
|
+
|
|
480
|
+
if (!emote.id.trim()) {
|
|
481
|
+
throw new Error(`Emote at index ${index} has an empty "id"`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!emote.name.trim()) {
|
|
485
|
+
throw new Error(`Emote at index ${index} has an empty "name"`)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (emote.duration < 0) {
|
|
489
|
+
throw new Error(`Emote at index ${index} has a negative "duration"`)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
id: emote.id.trim(),
|
|
494
|
+
name: emote.name.trim(),
|
|
495
|
+
duration: emote.duration
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
_validateEmoteProperty(emote, property, expectedType, index) {
|
|
500
|
+
if (!(property in emote)) {
|
|
501
|
+
throw new Error(`Emote at index ${index} is missing "${property}" property`)
|
|
502
|
+
}
|
|
503
|
+
if (typeof emote[property] !== expectedType) {
|
|
504
|
+
throw new Error(`Emote at index ${index} has invalid "${property}" type. Expected ${expectedType}`)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
module.exports = DanceFloor
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
class CleanupManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.timers = new Set();
|
|
4
|
+
this.intervals = new Set();
|
|
5
|
+
this.resources = new Set();
|
|
6
|
+
this.listeners = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
registerTimer(timer) {
|
|
10
|
+
this.timers.add(timer);
|
|
11
|
+
return timer;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerInterval(interval) {
|
|
15
|
+
this.intervals.add(interval);
|
|
16
|
+
return interval;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
registerResource(resource) {
|
|
20
|
+
this.resources.add(resource);
|
|
21
|
+
return resource;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
registerListener(emitter, event, listener) {
|
|
25
|
+
if (!this.listeners.has(emitter)) {
|
|
26
|
+
this.listeners.set(emitter, []);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.listeners.get(emitter).push({ event, listener });
|
|
30
|
+
emitter.on(event, listener);
|
|
31
|
+
|
|
32
|
+
return { emitter, event, listener };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
registerCleanupMethod(method) {
|
|
36
|
+
const resource = { cleanup: method };
|
|
37
|
+
this.resources.add(resource);
|
|
38
|
+
return resource;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
cleanup() {
|
|
42
|
+
// Clear all timers
|
|
43
|
+
for (const timer of this.timers) {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
this.timers.clear();
|
|
47
|
+
|
|
48
|
+
// Clear all intervals
|
|
49
|
+
for (const interval of this.intervals) {
|
|
50
|
+
clearInterval(interval);
|
|
51
|
+
}
|
|
52
|
+
this.intervals.clear();
|
|
53
|
+
|
|
54
|
+
// Remove all listeners
|
|
55
|
+
for (const [emitter, listeners] of this.listeners) {
|
|
56
|
+
for (const { event, listener } of listeners) {
|
|
57
|
+
emitter.removeListener(event, listener);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.listeners.clear();
|
|
61
|
+
|
|
62
|
+
// Clean up all resources
|
|
63
|
+
for (const resource of this.resources) {
|
|
64
|
+
try {
|
|
65
|
+
if (typeof resource.cleanup === 'function') {
|
|
66
|
+
resource.cleanup();
|
|
67
|
+
} else if (typeof resource.destroy === 'function') {
|
|
68
|
+
resource.destroy();
|
|
69
|
+
} else if (typeof resource.close === 'function') {
|
|
70
|
+
resource.close();
|
|
71
|
+
} else if (typeof resource.disconnect === 'function') {
|
|
72
|
+
resource.disconnect();
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Silently fail during cleanup
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
this.resources.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
removeTimer(timer) {
|
|
82
|
+
if (this.timers.has(timer)) {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
this.timers.delete(timer);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
removeInterval(interval) {
|
|
91
|
+
if (this.intervals.has(interval)) {
|
|
92
|
+
clearInterval(interval);
|
|
93
|
+
this.intervals.delete(interval);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
removeResource(resource) {
|
|
100
|
+
return this.resources.delete(resource);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
removeListener(emitter, event, listener) {
|
|
104
|
+
const listeners = this.listeners.get(emitter);
|
|
105
|
+
if (!listeners) return false;
|
|
106
|
+
|
|
107
|
+
const index = listeners.findIndex(l => l.event === event && l.listener === listener);
|
|
108
|
+
if (index !== -1) {
|
|
109
|
+
emitter.removeListener(event, listener);
|
|
110
|
+
listeners.splice(index, 1);
|
|
111
|
+
|
|
112
|
+
if (listeners.length === 0) {
|
|
113
|
+
this.listeners.delete(emitter);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getStats() {
|
|
121
|
+
return {
|
|
122
|
+
timers: this.timers.size,
|
|
123
|
+
intervals: this.intervals.size,
|
|
124
|
+
resources: this.resources.size,
|
|
125
|
+
listeners: Array.from(this.listeners.values()).reduce((sum, arr) => sum + arr.length, 0)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = CleanupManager;
|