poi-plugin-mcp 0.2.16 → 0.2.21

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.
@@ -1,439 +1,515 @@
1
- const { createPoiActionEvents } = require('./poi-action-events')
2
-
3
- const QUEST_LIST_PATH = '/kcsapi/api_get_member/questlist'
4
- const UNSET_SLOT_PATH = '/kcsapi/api_get_member/unsetslot'
5
- const QUEST_ACTION_PATHS = new Set([
6
- '/kcsapi/api_req_quest/start',
7
- '/kcsapi/api_req_quest/stop',
8
- ])
9
- const EQUIPMENT_ACTION_PATHS = new Set([
10
- '/kcsapi/api_req_kaisou/slotset',
11
- '/kcsapi/api_req_kaisou/slotset_ex',
12
- '/kcsapi/api_req_kaisou/slot_deprive',
13
- '/kcsapi/api_req_kaisou/unsetslot_all',
14
- ])
15
- const FLEET_ACTION_PATH = '/kcsapi/api_req_hensei/change'
16
- const BATTLE_RESULT_PATHS = new Set([
17
- '/kcsapi/api_req_practice/battle_result',
18
- '/kcsapi/api_req_sortie/battleresult',
19
- '/kcsapi/api_req_combined_battle/battleresult',
20
- ])
21
- const BATTLE_PATHS = new Set([
22
- '/kcsapi/api_req_practice/battle',
23
- '/kcsapi/api_req_practice/midnight_battle',
24
- '/kcsapi/api_req_sortie/battle',
25
- '/kcsapi/api_req_sortie/airbattle',
26
- '/kcsapi/api_req_sortie/ld_airbattle',
27
- '/kcsapi/api_req_sortie/ld_shooting',
28
- '/kcsapi/api_req_battle_midnight/battle',
29
- '/kcsapi/api_req_battle_midnight/sp_midnight',
30
- '/kcsapi/api_req_combined_battle/battle',
31
- '/kcsapi/api_req_combined_battle/battle_water',
32
- '/kcsapi/api_req_combined_battle/airbattle',
33
- '/kcsapi/api_req_combined_battle/ld_airbattle',
34
- '/kcsapi/api_req_combined_battle/ld_shooting',
35
- '/kcsapi/api_req_combined_battle/ec_battle',
36
- '/kcsapi/api_req_combined_battle/each_battle',
37
- '/kcsapi/api_req_combined_battle/each_battle_water',
38
- '/kcsapi/api_req_combined_battle/midnight_battle',
39
- '/kcsapi/api_req_combined_battle/sp_midnight',
40
- '/kcsapi/api_req_combined_battle/ec_midnight_battle',
41
- '/kcsapi/api_req_combined_battle/ec_night_to_day',
42
- ])
43
-
44
- function createPoiTelemetry(options = {}) {
45
- const now = options.now || (() => new Date())
46
- const actionEvents = createPoiActionEvents({ now })
47
- let questGeneration = 0
48
- let questList = null
49
- let questActionGeneration = 0
50
- let questAction = null
51
- let equipmentActionGeneration = 0
52
- let equipmentAction = null
53
- let unsetSlotGeneration = 0
54
- let unsetSlot = null
55
- let fleetActionGeneration = 0
56
- let fleetAction = null
57
- let battleGeneration = 0
58
- let battleTelemetry = null
59
-
60
- function handleGameResponse(event) {
61
- const detail = event && event.detail
62
- if (!detail || typeof detail.path !== 'string') return
63
- const actionEvent = actionEvents.capture(detail)
64
-
65
- if (detail.path === QUEST_LIST_PATH) {
66
- captureQuestList(detail, actionEvent)
67
- return
68
- }
69
- if (detail.path === UNSET_SLOT_PATH) {
70
- captureUnsetSlot(detail, actionEvent)
71
- return
72
- }
73
- if (QUEST_ACTION_PATHS.has(detail.path)) {
74
- captureQuestAction(detail, actionEvent)
75
- return
76
- }
77
- if (EQUIPMENT_ACTION_PATHS.has(detail.path)) {
78
- captureEquipmentAction(detail, actionEvent)
79
- return
80
- }
81
- if (detail.path === FLEET_ACTION_PATH) {
82
- captureFleetAction(detail, actionEvent)
83
- return
84
- }
85
- if (BATTLE_RESULT_PATHS.has(detail.path)) {
86
- captureBattleResult(detail)
87
- return
88
- }
89
- if (BATTLE_PATHS.has(detail.path)) {
90
- captureBattlePacket(detail)
91
- }
92
- }
93
-
94
- function captureQuestList(detail, actionEvent) {
95
- const body = detail.body
96
- const postBody = detail.postBody
97
- const tabId = toInteger(postBody && postBody.api_tab_id)
98
- if (
99
- !body ||
100
- !Array.isArray(body.api_list) ||
101
- tabId == null ||
102
- tabId < 0 ||
103
- tabId > 9
104
- ) {
105
- return
106
- }
107
-
108
- const quests = body.api_list.flatMap((quest, index) => {
109
- if (!quest || typeof quest !== 'object' || !Number.isInteger(quest.api_no)) {
110
- return []
111
- }
112
- return [{ ...quest, listIndex: index + 1 }]
113
- })
114
-
115
- questGeneration += 1
116
- questList = {
117
- available: true,
118
- generation: questGeneration,
119
- capturedAt: actionEvent
120
- ? actionEvent.capturedAt
121
- : now().toISOString(),
122
- tabId,
123
- pageNo: nonNegativeInteger(body.api_disp_page),
124
- pageCount: nonNegativeInteger(body.api_page_count),
125
- count: nonNegativeInteger(body.api_count),
126
- execCount: nonNegativeInteger(body.api_exec_count),
127
- execType: nonNegativeInteger(body.api_exec_type),
128
- quests,
129
- }
130
- }
131
-
132
- function captureQuestAction(detail, actionEvent) {
133
- const apiResult = actionEvent
134
- ? actionEvent.apiResult
135
- : equipmentApiResult(detail)
136
- if (apiResult !== 1) return
137
- const postBody = detail.postBody
138
- const questId = toInteger(postBody && postBody.api_quest_id)
139
- if (questId == null || questId <= 0) return
140
-
141
- questActionGeneration += 1
142
- questAction = {
143
- available: true,
144
- generation: questActionGeneration,
145
- capturedAt: actionEvent
146
- ? actionEvent.capturedAt
147
- : now().toISOString(),
148
- path: actionEvent ? actionEvent.path : detail.path,
149
- questId,
150
- flag: toInteger(postBody && postBody.api_quest_flag),
151
- apiResult,
152
- }
153
- }
154
-
155
- function captureEquipmentAction(detail, actionEvent) {
156
- const apiResult = actionEvent
157
- ? actionEvent.apiResult
158
- : equipmentApiResult(detail)
159
- if (apiResult === 0) return
160
- const postBody = normalizeEquipmentPostBody(detail.path, detail.postBody)
161
- if (!postBody) return
162
-
163
- equipmentActionGeneration += 1
164
- equipmentAction = {
165
- available: true,
166
- generation: equipmentActionGeneration,
167
- capturedAt: actionEvent
168
- ? actionEvent.capturedAt
169
- : now().toISOString(),
170
- path: actionEvent ? actionEvent.path : detail.path,
171
- apiResult,
172
- postBody,
173
- }
174
- }
175
-
176
- function captureUnsetSlot(detail, actionEvent) {
177
- const bySlotType = normalizeUnsetSlotBody(detail.body)
178
- if (bySlotType == null) return
179
-
180
- unsetSlotGeneration += 1
181
- unsetSlot = {
182
- available: true,
183
- generation: unsetSlotGeneration,
184
- capturedAt: actionEvent
185
- ? actionEvent.capturedAt
186
- : now().toISOString(),
187
- bySlotType,
188
- equipmentIds: Object.values(bySlotType)
189
- .flatMap((groups) => Object.values(groups))
190
- .flat(),
191
- }
192
- }
193
-
194
- function captureFleetAction(detail, actionEvent) {
195
- const apiResult = actionEvent
196
- ? actionEvent.apiResult
197
- : equipmentApiResult(detail)
198
- if (apiResult === 0) return
199
- const postBody = normalizeFleetPostBody(detail.postBody)
200
- if (!postBody) return
201
-
202
- fleetActionGeneration += 1
203
- fleetAction = {
204
- available: true,
205
- generation: fleetActionGeneration,
206
- capturedAt: actionEvent
207
- ? actionEvent.capturedAt
208
- : now().toISOString(),
209
- path: actionEvent ? actionEvent.path : detail.path,
210
- apiResult,
211
- postBody,
212
- }
213
- }
214
-
215
- function captureBattlePacket(detail) {
216
- if (!battleTelemetry || battleTelemetry.status === 'settled') {
217
- battleGeneration += 1
218
- }
219
- battleTelemetry = {
220
- available: true,
221
- generation: battleGeneration,
222
- status: 'in_progress',
223
- observed: {
224
- capturedAt: now().toISOString(),
225
- path: detail.path,
226
- time: finiteOrNull(detail.time),
227
- phaseStartHp: {
228
- friendlyMain: numberArray(detail.body && detail.body.api_f_nowhps),
229
- friendlyEscort: numberArray(
230
- detail.body && detail.body.api_f_nowhps_combined,
231
- ),
232
- enemyMain: numberArray(detail.body && detail.body.api_e_nowhps),
233
- enemyEscort: numberArray(
234
- detail.body && detail.body.api_e_nowhps_combined,
235
- ),
236
- },
237
- },
238
- official: null,
239
- }
240
- }
241
-
242
- function captureBattleResult(detail) {
243
- if (!battleTelemetry) battleGeneration += 1
244
- const body = detail.body && typeof detail.body === 'object'
245
- ? detail.body
246
- : {}
247
- battleTelemetry = {
248
- available: true,
249
- generation: battleGeneration,
250
- status: 'settled',
251
- observed: battleTelemetry ? battleTelemetry.observed : null,
252
- official: {
253
- capturedAt: now().toISOString(),
254
- path: detail.path,
255
- time: finiteOrNull(detail.time),
256
- rank: typeof body.api_win_rank === 'string' ? body.api_win_rank : null,
257
- mvpPosition: {
258
- main: positiveIntegerOrNull(body.api_mvp),
259
- escort: positiveIntegerOrNull(body.api_mvp_combined),
260
- },
261
- drop: {
262
- ship: objectOrNull(body.api_get_ship),
263
- useItem: objectOrNull(body.api_get_useitem),
264
- },
265
- },
266
- }
267
- }
268
-
269
- return {
270
- handleGameResponse,
271
- getQuestList() {
272
- return questList || { available: false, generation: 0 }
273
- },
274
- getQuestAction() {
275
- return questAction || { available: false, generation: 0 }
276
- },
277
- getEquipmentAction() {
278
- return equipmentAction || { available: false, generation: 0 }
279
- },
280
- getUnsetSlot() {
281
- return unsetSlot || { available: false, generation: 0 }
282
- },
283
- getFleetAction() {
284
- return fleetAction || { available: false, generation: 0 }
285
- },
286
- getActionEvents(options) {
287
- return actionEvents.read(options)
288
- },
289
- getBattleTelemetry() {
290
- return battleTelemetry || { available: false, generation: 0 }
291
- },
292
- }
293
- }
294
-
295
- function normalizeUnsetSlotBody(body) {
296
- if (!body || typeof body !== 'object' || Array.isArray(body)) return null
297
- const source = body.api_data &&
298
- typeof body.api_data === 'object' &&
299
- !Array.isArray(body.api_data)
300
- ? body.api_data
301
- : body
302
- const output = {}
303
- let totalIds = 0
304
-
305
- for (const [slotType, groups] of Object.entries(source)) {
306
- if (!/^api_slottype\d+$/.test(slotType)) continue
307
- if (!groups || typeof groups !== 'object' || Array.isArray(groups)) continue
308
- const normalizedGroups = {}
309
- for (const [groupId, ids] of Object.entries(groups)) {
310
- if (!/^\d+$/.test(groupId) || !Array.isArray(ids)) continue
311
- const normalizedIds = ids
312
- .map(positiveIntegerOrNull)
313
- .filter((id) => id != null)
314
- totalIds += normalizedIds.length
315
- if (totalIds > 10000) return null
316
- normalizedGroups[groupId] = normalizedIds
317
- }
318
- if (Object.keys(normalizedGroups).length > 0) {
319
- output[slotType] = normalizedGroups
320
- }
321
- }
322
- return Object.keys(output).length > 0 ? output : null
323
- }
324
-
325
- function normalizeFleetPostBody(postBody) {
326
- if (!postBody || typeof postBody !== 'object') return null
327
- const fleetId = boundedInteger(postBody.api_id, 1, 4)
328
- const shipIndex = boundedInteger(postBody.api_ship_idx, 0, 5)
329
- const shipId = boundedInteger(postBody.api_ship_id, -1)
330
- if (fleetId == null || shipIndex == null || shipId == null) return null
331
- return {
332
- api_id: fleetId,
333
- api_ship_idx: shipIndex,
334
- api_ship_id: shipId,
335
- }
336
- }
337
-
338
- function normalizeEquipmentPostBody(path, postBody) {
339
- if (!postBody || typeof postBody !== 'object') return null
340
- if (path === '/kcsapi/api_req_kaisou/slotset') {
341
- const shipId = positiveIntegerOrNull(postBody.api_id)
342
- const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 4)
343
- const itemId = boundedInteger(postBody.api_item_id, -1)
344
- if (shipId == null || slotIndex == null || itemId == null) return null
345
- return {
346
- api_id: shipId,
347
- api_slot_idx: slotIndex,
348
- api_item_id: itemId,
349
- }
350
- }
351
- if (path === '/kcsapi/api_req_kaisou/slotset_ex') {
352
- const shipId = positiveIntegerOrNull(postBody.api_id)
353
- const itemId = boundedInteger(postBody.api_item_id, -1)
354
- if (shipId == null || itemId == null) return null
355
- return {
356
- api_id: shipId,
357
- api_item_id: itemId,
358
- }
359
- }
360
- if (path === '/kcsapi/api_req_kaisou/slot_deprive') {
361
- const setShip = positiveIntegerOrNull(postBody.api_set_ship)
362
- const unsetShip = positiveIntegerOrNull(postBody.api_unset_ship)
363
- const setIndex = boundedInteger(postBody.api_set_idx, -1, 4)
364
- const unsetIndex = boundedInteger(postBody.api_unset_idx, -1, 4)
365
- if (
366
- setShip == null ||
367
- unsetShip == null ||
368
- setIndex == null ||
369
- unsetIndex == null
370
- ) {
371
- return null
372
- }
373
- return {
374
- api_set_ship: setShip,
375
- api_unset_ship: unsetShip,
376
- api_set_idx: setIndex,
377
- api_unset_idx: unsetIndex,
378
- }
379
- }
380
- if (path === '/kcsapi/api_req_kaisou/unsetslot_all') {
381
- const shipId = positiveIntegerOrNull(postBody.api_id)
382
- return shipId == null ? null : { api_id: shipId }
383
- }
384
- return null
385
- }
386
-
387
- function equipmentApiResult(detail) {
388
- const candidates = [
389
- detail.apiResult,
390
- detail.api_result,
391
- detail.result,
392
- detail.body && detail.body.api_result,
393
- ]
394
- for (const candidate of candidates) {
395
- const value = toInteger(candidate)
396
- if (value != null) return value === 1 ? 1 : 0
397
- }
398
- return null
399
- }
400
-
401
- function boundedInteger(value, minimum, maximum = Number.MAX_SAFE_INTEGER) {
402
- const parsed = toInteger(value)
403
- return parsed != null && parsed >= minimum && parsed <= maximum
404
- ? parsed
405
- : null
406
- }
407
-
408
- function toInteger(value, fallback = null) {
409
- if (value == null || value === '') return fallback
410
- const parsed = Number(value)
411
- return Number.isInteger(parsed) ? parsed : null
412
- }
413
-
414
- function nonNegativeInteger(value) {
415
- const parsed = toInteger(value, 0)
416
- return parsed != null && parsed >= 0 ? parsed : 0
417
- }
418
-
419
- function positiveIntegerOrNull(value) {
420
- const parsed = toInteger(value)
421
- return parsed != null && parsed > 0 ? parsed : null
422
- }
423
-
424
- function finiteOrNull(value) {
425
- return Number.isFinite(value) ? value : null
426
- }
427
-
428
- function numberArray(value) {
429
- if (!Array.isArray(value)) return []
430
- return value.map((item) => finiteOrNull(item))
431
- }
432
-
433
- function objectOrNull(value) {
434
- return value && typeof value === 'object' ? { ...value } : null
435
- }
436
-
437
- module.exports = {
438
- createPoiTelemetry,
439
- }
1
+ const { createPoiActionEvents } = require('./poi-action-events')
2
+ const { createPoiApiResponses } = require('./poi-api-responses')
3
+
4
+ const QUEST_LIST_PATH = '/kcsapi/api_get_member/questlist'
5
+ const UNSET_SLOT_PATH = '/kcsapi/api_get_member/unsetslot'
6
+ const EQUIPMENT_SELECTION_PATH = '/kcsapi/api_req_kaisou/slot_select'
7
+ const QUEST_ACTION_PATHS = new Set([
8
+ '/kcsapi/api_req_quest/start',
9
+ '/kcsapi/api_req_quest/stop',
10
+ '/kcsapi/api_req_quest/clearitemget',
11
+ ])
12
+ const EQUIPMENT_ACTION_PATHS = new Set([
13
+ '/kcsapi/api_req_kaisou/slotset',
14
+ '/kcsapi/api_req_kaisou/slotset_ex',
15
+ '/kcsapi/api_req_kaisou/slot_deprive',
16
+ '/kcsapi/api_req_kaisou/unsetslot_all',
17
+ ])
18
+ const FLEET_ACTION_PATH = '/kcsapi/api_req_hensei/change'
19
+ const BATTLE_RESULT_PATHS = new Set([
20
+ '/kcsapi/api_req_practice/battle_result',
21
+ '/kcsapi/api_req_sortie/battleresult',
22
+ '/kcsapi/api_req_combined_battle/battleresult',
23
+ ])
24
+ const BATTLE_PATHS = new Set([
25
+ '/kcsapi/api_req_practice/battle',
26
+ '/kcsapi/api_req_practice/midnight_battle',
27
+ '/kcsapi/api_req_sortie/battle',
28
+ '/kcsapi/api_req_sortie/airbattle',
29
+ '/kcsapi/api_req_sortie/ld_airbattle',
30
+ '/kcsapi/api_req_sortie/ld_shooting',
31
+ '/kcsapi/api_req_battle_midnight/battle',
32
+ '/kcsapi/api_req_battle_midnight/sp_midnight',
33
+ '/kcsapi/api_req_combined_battle/battle',
34
+ '/kcsapi/api_req_combined_battle/battle_water',
35
+ '/kcsapi/api_req_combined_battle/airbattle',
36
+ '/kcsapi/api_req_combined_battle/ld_airbattle',
37
+ '/kcsapi/api_req_combined_battle/ld_shooting',
38
+ '/kcsapi/api_req_combined_battle/ec_battle',
39
+ '/kcsapi/api_req_combined_battle/each_battle',
40
+ '/kcsapi/api_req_combined_battle/each_battle_water',
41
+ '/kcsapi/api_req_combined_battle/midnight_battle',
42
+ '/kcsapi/api_req_combined_battle/sp_midnight',
43
+ '/kcsapi/api_req_combined_battle/ec_midnight_battle',
44
+ '/kcsapi/api_req_combined_battle/ec_night_to_day',
45
+ ])
46
+
47
+ function createPoiTelemetry(options = {}) {
48
+ const now = options.now || (() => new Date())
49
+ const actionEvents = createPoiActionEvents({ now })
50
+ const apiResponses = createPoiApiResponses({ now })
51
+ let questGeneration = 0
52
+ let questList = null
53
+ let questActionGeneration = 0
54
+ let questAction = null
55
+ let equipmentActionGeneration = 0
56
+ let equipmentAction = null
57
+ let equipmentSelectionGeneration = 0
58
+ let equipmentSelection = null
59
+ let unsetSlotGeneration = 0
60
+ let unsetSlot = null
61
+ let fleetActionGeneration = 0
62
+ let fleetAction = null
63
+ let battleGeneration = 0
64
+ let battleTelemetry = null
65
+
66
+ function handleGameResponse(event) {
67
+ const detail = event && event.detail
68
+ if (!detail || typeof detail.path !== 'string') return
69
+ const actionEvent = actionEvents.capture(detail)
70
+ apiResponses.capture(detail, actionEvent && actionEvent.capturedAt)
71
+
72
+ if (detail.path === QUEST_LIST_PATH) {
73
+ captureQuestList(detail, actionEvent)
74
+ return
75
+ }
76
+ if (detail.path === UNSET_SLOT_PATH) {
77
+ captureUnsetSlot(detail, actionEvent)
78
+ return
79
+ }
80
+ if (detail.path === EQUIPMENT_SELECTION_PATH) {
81
+ captureEquipmentSelection(detail, actionEvent)
82
+ return
83
+ }
84
+ if (QUEST_ACTION_PATHS.has(detail.path)) {
85
+ captureQuestAction(detail, actionEvent)
86
+ return
87
+ }
88
+ if (EQUIPMENT_ACTION_PATHS.has(detail.path)) {
89
+ captureEquipmentAction(detail, actionEvent)
90
+ return
91
+ }
92
+ if (detail.path === FLEET_ACTION_PATH) {
93
+ captureFleetAction(detail, actionEvent)
94
+ return
95
+ }
96
+ if (BATTLE_RESULT_PATHS.has(detail.path)) {
97
+ captureBattleResult(detail)
98
+ return
99
+ }
100
+ if (BATTLE_PATHS.has(detail.path)) {
101
+ captureBattlePacket(detail)
102
+ }
103
+ }
104
+
105
+ function captureQuestList(detail, actionEvent) {
106
+ const body = detail.body
107
+ const postBody = detail.postBody
108
+ const tabId = toInteger(postBody && postBody.api_tab_id)
109
+ if (
110
+ !body ||
111
+ !Array.isArray(body.api_list) ||
112
+ tabId == null ||
113
+ tabId < 0 ||
114
+ tabId > 9
115
+ ) {
116
+ return
117
+ }
118
+
119
+ const quests = body.api_list.flatMap((quest, index) => {
120
+ if (!quest || typeof quest !== 'object' || !Number.isInteger(quest.api_no)) {
121
+ return []
122
+ }
123
+ return [{ ...quest, listIndex: index + 1 }]
124
+ })
125
+
126
+ questGeneration += 1
127
+ questList = {
128
+ available: true,
129
+ generation: questGeneration,
130
+ capturedAt: actionEvent
131
+ ? actionEvent.capturedAt
132
+ : now().toISOString(),
133
+ tabId,
134
+ pageNo: nonNegativeInteger(body.api_disp_page),
135
+ pageCount: nonNegativeInteger(body.api_page_count),
136
+ count: nonNegativeInteger(body.api_count),
137
+ execCount: nonNegativeInteger(body.api_exec_count),
138
+ execType: nonNegativeInteger(body.api_exec_type),
139
+ quests,
140
+ }
141
+ }
142
+
143
+ function captureQuestAction(detail, actionEvent) {
144
+ const apiResult = actionEvent
145
+ ? actionEvent.apiResult
146
+ : equipmentApiResult(detail)
147
+ if (apiResult !== 1) return
148
+ const postBody = detail.postBody
149
+ const questId = toInteger(postBody && postBody.api_quest_id)
150
+ if (questId == null || questId <= 0) return
151
+
152
+ questActionGeneration += 1
153
+ questAction = {
154
+ available: true,
155
+ generation: questActionGeneration,
156
+ capturedAt: actionEvent
157
+ ? actionEvent.capturedAt
158
+ : now().toISOString(),
159
+ path: actionEvent ? actionEvent.path : detail.path,
160
+ questId,
161
+ flag: toInteger(postBody && postBody.api_quest_flag),
162
+ selectedKind: selectedKindOf(postBody),
163
+ apiResult,
164
+ }
165
+ }
166
+
167
+ function captureEquipmentAction(detail, actionEvent) {
168
+ const apiResult = actionEvent
169
+ ? actionEvent.apiResult
170
+ : equipmentApiResult(detail)
171
+ if (apiResult === 0) return
172
+ const postBody = normalizeEquipmentPostBody(detail.path, detail.postBody)
173
+ if (!postBody) return
174
+
175
+ equipmentActionGeneration += 1
176
+ equipmentAction = {
177
+ available: true,
178
+ generation: equipmentActionGeneration,
179
+ capturedAt: actionEvent
180
+ ? actionEvent.capturedAt
181
+ : now().toISOString(),
182
+ path: actionEvent ? actionEvent.path : detail.path,
183
+ apiResult,
184
+ postBody,
185
+ }
186
+ }
187
+
188
+ function captureUnsetSlot(detail, actionEvent) {
189
+ const bySlotType = normalizeUnsetSlotBody(detail.body)
190
+ if (bySlotType == null) return
191
+
192
+ unsetSlotGeneration += 1
193
+ unsetSlot = {
194
+ available: true,
195
+ generation: unsetSlotGeneration,
196
+ capturedAt: actionEvent
197
+ ? actionEvent.capturedAt
198
+ : now().toISOString(),
199
+ bySlotType,
200
+ equipmentIds: Object.values(bySlotType)
201
+ .flatMap((groups) => Object.values(groups))
202
+ .flat(),
203
+ }
204
+ }
205
+
206
+ function captureEquipmentSelection(detail, actionEvent) {
207
+ const equipmentIds = normalizeEquipmentSelectionBody(detail.body)
208
+ if (equipmentIds == null) return
209
+ const postBody = normalizeEquipmentSelectionPostBody(detail.postBody)
210
+ if (!postBody) return
211
+
212
+ equipmentSelectionGeneration += 1
213
+ equipmentSelection = {
214
+ available: true,
215
+ generation: equipmentSelectionGeneration,
216
+ capturedAt: actionEvent
217
+ ? actionEvent.capturedAt
218
+ : now().toISOString(),
219
+ path: actionEvent ? actionEvent.path : detail.path,
220
+ postBody,
221
+ equipmentIds,
222
+ count: equipmentIds.length,
223
+ }
224
+ }
225
+
226
+ function captureFleetAction(detail, actionEvent) {
227
+ const apiResult = actionEvent
228
+ ? actionEvent.apiResult
229
+ : equipmentApiResult(detail)
230
+ if (apiResult === 0) return
231
+ const postBody = normalizeFleetPostBody(detail.postBody)
232
+ if (!postBody) return
233
+
234
+ fleetActionGeneration += 1
235
+ fleetAction = {
236
+ available: true,
237
+ generation: fleetActionGeneration,
238
+ capturedAt: actionEvent
239
+ ? actionEvent.capturedAt
240
+ : now().toISOString(),
241
+ path: actionEvent ? actionEvent.path : detail.path,
242
+ apiResult,
243
+ postBody,
244
+ }
245
+ }
246
+
247
+ function captureBattlePacket(detail) {
248
+ if (!battleTelemetry || battleTelemetry.status === 'settled') {
249
+ battleGeneration += 1
250
+ }
251
+ battleTelemetry = {
252
+ available: true,
253
+ generation: battleGeneration,
254
+ status: 'in_progress',
255
+ observed: {
256
+ capturedAt: now().toISOString(),
257
+ path: detail.path,
258
+ time: finiteOrNull(detail.time),
259
+ phaseStartHp: {
260
+ friendlyMain: numberArray(detail.body && detail.body.api_f_nowhps),
261
+ friendlyEscort: numberArray(
262
+ detail.body && detail.body.api_f_nowhps_combined,
263
+ ),
264
+ enemyMain: numberArray(detail.body && detail.body.api_e_nowhps),
265
+ enemyEscort: numberArray(
266
+ detail.body && detail.body.api_e_nowhps_combined,
267
+ ),
268
+ },
269
+ },
270
+ official: null,
271
+ }
272
+ }
273
+
274
+ function captureBattleResult(detail) {
275
+ if (!battleTelemetry) battleGeneration += 1
276
+ const body = detail.body && typeof detail.body === 'object'
277
+ ? detail.body
278
+ : {}
279
+ battleTelemetry = {
280
+ available: true,
281
+ generation: battleGeneration,
282
+ status: 'settled',
283
+ observed: battleTelemetry ? battleTelemetry.observed : null,
284
+ official: {
285
+ capturedAt: now().toISOString(),
286
+ path: detail.path,
287
+ time: finiteOrNull(detail.time),
288
+ rank: typeof body.api_win_rank === 'string' ? body.api_win_rank : null,
289
+ mvpPosition: {
290
+ main: positiveIntegerOrNull(body.api_mvp),
291
+ escort: positiveIntegerOrNull(body.api_mvp_combined),
292
+ },
293
+ drop: {
294
+ ship: objectOrNull(body.api_get_ship),
295
+ useItem: objectOrNull(body.api_get_useitem),
296
+ },
297
+ },
298
+ }
299
+ }
300
+
301
+ return {
302
+ handleGameResponse,
303
+ getQuestList() {
304
+ return questList || { available: false, generation: 0 }
305
+ },
306
+ getQuestAction() {
307
+ return questAction || { available: false, generation: 0 }
308
+ },
309
+ getEquipmentAction() {
310
+ return equipmentAction || { available: false, generation: 0 }
311
+ },
312
+ getEquipmentSelection() {
313
+ return equipmentSelection || { available: false, generation: 0 }
314
+ },
315
+ getUnsetSlot() {
316
+ return unsetSlot || { available: false, generation: 0 }
317
+ },
318
+ getFleetAction() {
319
+ return fleetAction || { available: false, generation: 0 }
320
+ },
321
+ getActionEvents(options) {
322
+ return actionEvents.read(options)
323
+ },
324
+ getActionEventsWait(options) {
325
+ return actionEvents.wait(options)
326
+ },
327
+ getApiResponses(options) {
328
+ return apiResponses.read(options)
329
+ },
330
+ getBattleTelemetry() {
331
+ return battleTelemetry || { available: false, generation: 0 }
332
+ },
333
+ }
334
+ }
335
+
336
+ function normalizeEquipmentSelectionBody(body) {
337
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null
338
+ const source = body.api_data &&
339
+ typeof body.api_data === 'object' &&
340
+ !Array.isArray(body.api_data)
341
+ ? body.api_data
342
+ : body
343
+ if (!Array.isArray(source.api_slotitem)) return null
344
+ const ids = source.api_slotitem
345
+ .map((item) => item && typeof item === 'object' && !Array.isArray(item)
346
+ ? positiveIntegerOrNull(item.api_id)
347
+ : null)
348
+ if (ids.some((id) => id == null) || ids.length > 10000) return null
349
+ return ids
350
+ }
351
+
352
+ function normalizeEquipmentSelectionPostBody(postBody) {
353
+ if (!postBody || typeof postBody !== 'object') return null
354
+ const shipId = positiveIntegerOrNull(postBody.api_id)
355
+ const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 5)
356
+ if (shipId == null || slotIndex == null) return null
357
+ return {
358
+ api_id: shipId,
359
+ api_slot_idx: slotIndex,
360
+ }
361
+ }
362
+
363
+ function normalizeUnsetSlotBody(body) {
364
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null
365
+ const source = body.api_data &&
366
+ typeof body.api_data === 'object' &&
367
+ !Array.isArray(body.api_data)
368
+ ? body.api_data
369
+ : body
370
+ const output = {}
371
+ let totalIds = 0
372
+
373
+ for (const [slotType, groups] of Object.entries(source)) {
374
+ if (!/^api_slottype\d+$/.test(slotType)) continue
375
+ if (!groups || typeof groups !== 'object' || Array.isArray(groups)) continue
376
+ const normalizedGroups = {}
377
+ for (const [groupId, ids] of Object.entries(groups)) {
378
+ if (!/^\d+$/.test(groupId) || !Array.isArray(ids)) continue
379
+ const normalizedIds = ids
380
+ .map(positiveIntegerOrNull)
381
+ .filter((id) => id != null)
382
+ totalIds += normalizedIds.length
383
+ if (totalIds > 10000) return null
384
+ normalizedGroups[groupId] = normalizedIds
385
+ }
386
+ if (Object.keys(normalizedGroups).length > 0) {
387
+ output[slotType] = normalizedGroups
388
+ }
389
+ }
390
+ return Object.keys(output).length > 0 ? output : null
391
+ }
392
+
393
+ function normalizeFleetPostBody(postBody) {
394
+ if (!postBody || typeof postBody !== 'object') return null
395
+ const fleetId = boundedInteger(postBody.api_id, 1, 4)
396
+ const shipIndex = boundedInteger(postBody.api_ship_idx, 0, 5)
397
+ const shipId = boundedInteger(postBody.api_ship_id, -1)
398
+ if (fleetId == null || shipIndex == null || shipId == null) return null
399
+ return {
400
+ api_id: fleetId,
401
+ api_ship_idx: shipIndex,
402
+ api_ship_id: shipId,
403
+ }
404
+ }
405
+
406
+ function normalizeEquipmentPostBody(path, postBody) {
407
+ if (!postBody || typeof postBody !== 'object') return null
408
+ if (path === '/kcsapi/api_req_kaisou/slotset') {
409
+ const shipId = positiveIntegerOrNull(postBody.api_id)
410
+ const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 4)
411
+ const itemId = boundedInteger(postBody.api_item_id, -1)
412
+ if (shipId == null || slotIndex == null || itemId == null) return null
413
+ return {
414
+ api_id: shipId,
415
+ api_slot_idx: slotIndex,
416
+ api_item_id: itemId,
417
+ }
418
+ }
419
+ if (path === '/kcsapi/api_req_kaisou/slotset_ex') {
420
+ const shipId = positiveIntegerOrNull(postBody.api_id)
421
+ const itemId = boundedInteger(postBody.api_item_id, -1)
422
+ if (shipId == null || itemId == null) return null
423
+ return {
424
+ api_id: shipId,
425
+ api_item_id: itemId,
426
+ }
427
+ }
428
+ if (path === '/kcsapi/api_req_kaisou/slot_deprive') {
429
+ const setShip = positiveIntegerOrNull(postBody.api_set_ship)
430
+ const unsetShip = positiveIntegerOrNull(postBody.api_unset_ship)
431
+ const setIndex = boundedInteger(postBody.api_set_idx, -1, 4)
432
+ const unsetIndex = boundedInteger(postBody.api_unset_idx, -1, 4)
433
+ if (
434
+ setShip == null ||
435
+ unsetShip == null ||
436
+ setIndex == null ||
437
+ unsetIndex == null
438
+ ) {
439
+ return null
440
+ }
441
+ return {
442
+ api_set_ship: setShip,
443
+ api_unset_ship: unsetShip,
444
+ api_set_idx: setIndex,
445
+ api_unset_idx: unsetIndex,
446
+ }
447
+ }
448
+ if (path === '/kcsapi/api_req_kaisou/unsetslot_all') {
449
+ const shipId = positiveIntegerOrNull(postBody.api_id)
450
+ return shipId == null ? null : { api_id: shipId }
451
+ }
452
+ return null
453
+ }
454
+
455
+ function equipmentApiResult(detail) {
456
+ const candidates = [
457
+ detail.apiResult,
458
+ detail.api_result,
459
+ detail.result,
460
+ detail.body && detail.body.api_result,
461
+ ]
462
+ for (const candidate of candidates) {
463
+ const value = toInteger(candidate)
464
+ if (value != null) return value === 1 ? 1 : 0
465
+ }
466
+ return null
467
+ }
468
+
469
+ function boundedInteger(value, minimum, maximum = Number.MAX_SAFE_INTEGER) {
470
+ const parsed = toInteger(value)
471
+ return parsed != null && parsed >= minimum && parsed <= maximum
472
+ ? parsed
473
+ : null
474
+ }
475
+
476
+ function selectedKindOf(postBody) {
477
+ if (!postBody || typeof postBody !== 'object') return null
478
+ const value = postBody.api_selected_kind
479
+ if (value == null || value === '') return null
480
+ if (typeof value === 'string' || typeof value === 'number') return String(value)
481
+ return null
482
+ }
483
+
484
+ function toInteger(value, fallback = null) {
485
+ if (value == null || value === '') return fallback
486
+ const parsed = Number(value)
487
+ return Number.isInteger(parsed) ? parsed : null
488
+ }
489
+
490
+ function nonNegativeInteger(value) {
491
+ const parsed = toInteger(value, 0)
492
+ return parsed != null && parsed >= 0 ? parsed : 0
493
+ }
494
+
495
+ function positiveIntegerOrNull(value) {
496
+ const parsed = toInteger(value)
497
+ return parsed != null && parsed > 0 ? parsed : null
498
+ }
499
+
500
+ function finiteOrNull(value) {
501
+ return Number.isFinite(value) ? value : null
502
+ }
503
+
504
+ function numberArray(value) {
505
+ if (!Array.isArray(value)) return []
506
+ return value.map((item) => finiteOrNull(item))
507
+ }
508
+
509
+ function objectOrNull(value) {
510
+ return value && typeof value === 'object' ? { ...value } : null
511
+ }
512
+
513
+ module.exports = {
514
+ createPoiTelemetry,
515
+ }