poi-plugin-mcp 0.2.10 → 0.2.12
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/README.md +5 -1
- package/index.js +2 -0
- package/lib/bridge-controller.js +2 -0
- package/lib/poi-http-bridge.js +67 -2
- package/lib/poi-telemetry.js +142 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,11 +61,15 @@ token is never returned by an HTTP endpoint.
|
|
|
61
61
|
| `/equipment` | Raw owned equipment instance data |
|
|
62
62
|
| `/resources` | Resource array |
|
|
63
63
|
| `/quests` | Active quests and quest records |
|
|
64
|
+
| `/quest-list` | Latest complete quest-page telemetry |
|
|
65
|
+
| `/quest-action` | Latest successful quest start or stop request |
|
|
66
|
+
| `/equipment-action` | Latest successful equipment mutation request |
|
|
64
67
|
| `/airbase` | Land base air squadron data |
|
|
65
68
|
| `/names` | Ship, equipment, and mission name maps |
|
|
66
|
-
| `/master` | Master ship, equipment, ship type, equipment type, and
|
|
69
|
+
| `/master` | Master ship, equipment, ship type, equipment type, mission, and bounded equipment compatibility data |
|
|
67
70
|
| `/event` | Event ship tag definitions plus owned ships' current sally area |
|
|
68
71
|
| `/planner` | Ship Info deck planner areas and ship assignments |
|
|
72
|
+
| `/battle` | Observed battle packets, official settlement, and compact Prophet state |
|
|
69
73
|
| `/screenshot` | In-memory PNG capture of the game WebView |
|
|
70
74
|
| `/input/status` | Whether authenticated WebView input is enabled |
|
|
71
75
|
| `/input` | Authenticated, serialized WebView input |
|
package/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const telemetry = createPoiTelemetry()
|
|
|
6
6
|
const controller = createBridgeController({
|
|
7
7
|
getQuestList: telemetry.getQuestList,
|
|
8
8
|
getQuestAction: telemetry.getQuestAction,
|
|
9
|
+
getEquipmentAction: telemetry.getEquipmentAction,
|
|
10
|
+
getFleetAction: telemetry.getFleetAction,
|
|
9
11
|
getBattleTelemetry: telemetry.getBattleTelemetry,
|
|
10
12
|
})
|
|
11
13
|
const settingsClass = createSettingsClass(controller)
|
package/lib/bridge-controller.js
CHANGED
|
@@ -32,6 +32,8 @@ function createBridgeController(options = {}) {
|
|
|
32
32
|
performInput: options.performInput,
|
|
33
33
|
getQuestList: options.getQuestList,
|
|
34
34
|
getQuestAction: options.getQuestAction,
|
|
35
|
+
getEquipmentAction: options.getEquipmentAction,
|
|
36
|
+
getFleetAction: options.getFleetAction,
|
|
35
37
|
getBattleTelemetry: options.getBattleTelemetry,
|
|
36
38
|
inputEnabled: settings.inputEnabled,
|
|
37
39
|
inputToken,
|
package/lib/poi-http-bridge.js
CHANGED
|
@@ -15,18 +15,30 @@ const DEFAULT_PLANNER_FILE = path.join(
|
|
|
15
15
|
'poi',
|
|
16
16
|
'poi-plugin-ship-info.json',
|
|
17
17
|
)
|
|
18
|
+
const DEFAULT_MASTER_FILE = path.join(
|
|
19
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
20
|
+
'poi',
|
|
21
|
+
'navy-album',
|
|
22
|
+
'master.json',
|
|
23
|
+
)
|
|
18
24
|
const JSONRPC_VERSION = '2.0'
|
|
19
25
|
const MCP_PROTOCOL_VERSION = '2024-11-05'
|
|
20
26
|
const INPUT_BODY_LIMIT = 64 * 1024
|
|
27
|
+
const MASTER_FILE_LIMIT = 16 * 1024 * 1024
|
|
21
28
|
|
|
22
29
|
function createPoiDataBridge(options = {}) {
|
|
23
30
|
const getStore = options.getStore || defaultGetStore
|
|
24
31
|
const configuredPort = options.port == null ? DEFAULT_PORT : options.port
|
|
25
32
|
const portFile = options.portFile || DEFAULT_PORT_FILE
|
|
26
33
|
const plannerFile = options.plannerFile || DEFAULT_PLANNER_FILE
|
|
34
|
+
const masterFile = options.masterFile || DEFAULT_MASTER_FILE
|
|
27
35
|
const logger = options.logger || console
|
|
28
36
|
const getQuestList = options.getQuestList || (() => ({ available: false, generation: 0 }))
|
|
29
37
|
const getQuestAction = options.getQuestAction || (() => ({ available: false, generation: 0 }))
|
|
38
|
+
const getEquipmentAction = options.getEquipmentAction ||
|
|
39
|
+
(() => ({ available: false, generation: 0 }))
|
|
40
|
+
const getFleetAction = options.getFleetAction ||
|
|
41
|
+
(() => ({ available: false, generation: 0 }))
|
|
30
42
|
const getBattleTelemetry = options.getBattleTelemetry ||
|
|
31
43
|
(() => ({ available: false, generation: 0 }))
|
|
32
44
|
const inputEnabled = options.inputEnabled === true
|
|
@@ -242,6 +254,16 @@ function createPoiDataBridge(options = {}) {
|
|
|
242
254
|
return
|
|
243
255
|
}
|
|
244
256
|
|
|
257
|
+
if (endpoint === '/equipment-action') {
|
|
258
|
+
sendJson(res, 200, getEquipmentAction())
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (endpoint === '/fleet-action') {
|
|
263
|
+
sendJson(res, 200, getFleetAction())
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
245
267
|
const store = readStore()
|
|
246
268
|
const info = store.info
|
|
247
269
|
|
|
@@ -274,7 +296,7 @@ function createPoiDataBridge(options = {}) {
|
|
|
274
296
|
sendJson(res, 200, extractNames(store))
|
|
275
297
|
break
|
|
276
298
|
case '/master':
|
|
277
|
-
sendJson(res, 200, extractMasterData(store))
|
|
299
|
+
sendJson(res, 200, extractMasterData(store, masterFile))
|
|
278
300
|
break
|
|
279
301
|
case '/event':
|
|
280
302
|
sendJson(res, 200, extractEventData(store))
|
|
@@ -906,7 +928,7 @@ function collectSimpleNames(source, target) {
|
|
|
906
928
|
}
|
|
907
929
|
}
|
|
908
930
|
|
|
909
|
-
function extractMasterData(store) {
|
|
931
|
+
function extractMasterData(store, masterFile = DEFAULT_MASTER_FILE) {
|
|
910
932
|
const constants = store.const || {}
|
|
911
933
|
|
|
912
934
|
return {
|
|
@@ -920,9 +942,52 @@ function extractMasterData(store) {
|
|
|
920
942
|
constants.$slotItemTypes ||
|
|
921
943
|
{},
|
|
922
944
|
missions: constants.$missions || {},
|
|
945
|
+
equipmentRules: readEquipmentRules(masterFile),
|
|
923
946
|
}
|
|
924
947
|
}
|
|
925
948
|
|
|
949
|
+
function readEquipmentRules(masterFile) {
|
|
950
|
+
try {
|
|
951
|
+
const stat = fs.statSync(masterFile)
|
|
952
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MASTER_FILE_LIMIT) {
|
|
953
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
954
|
+
}
|
|
955
|
+
const master = JSON.parse(fs.readFileSync(masterFile, 'utf8'))
|
|
956
|
+
const equipmentShip = objectOrEmpty(master.api_mst_equip_ship)
|
|
957
|
+
const equipmentExslotTypes = Array.isArray(master.api_mst_equip_exslot)
|
|
958
|
+
? master.api_mst_equip_exslot.filter(Number.isInteger)
|
|
959
|
+
: []
|
|
960
|
+
const equipmentExslotShip = objectOrEmpty(
|
|
961
|
+
master.api_mst_equip_exslot_ship,
|
|
962
|
+
)
|
|
963
|
+
const equipmentLimitExslot = objectOrEmpty(
|
|
964
|
+
master.api_mst_equip_limit_exslot,
|
|
965
|
+
)
|
|
966
|
+
if (
|
|
967
|
+
Object.keys(equipmentShip).length === 0 ||
|
|
968
|
+
equipmentExslotTypes.length === 0
|
|
969
|
+
) {
|
|
970
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
available: true,
|
|
974
|
+
source: 'navy-album-master-cache',
|
|
975
|
+
equipmentShip,
|
|
976
|
+
equipmentExslotTypes,
|
|
977
|
+
equipmentExslotShip,
|
|
978
|
+
equipmentLimitExslot,
|
|
979
|
+
}
|
|
980
|
+
} catch (_) {
|
|
981
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function objectOrEmpty(value) {
|
|
986
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
987
|
+
? value
|
|
988
|
+
: {}
|
|
989
|
+
}
|
|
990
|
+
|
|
926
991
|
function extractEventData(store) {
|
|
927
992
|
const tags = extractShipTags(store)
|
|
928
993
|
const ships = {}
|
package/lib/poi-telemetry.js
CHANGED
|
@@ -3,6 +3,13 @@ const QUEST_ACTION_PATHS = new Set([
|
|
|
3
3
|
'/kcsapi/api_req_quest/start',
|
|
4
4
|
'/kcsapi/api_req_quest/stop',
|
|
5
5
|
])
|
|
6
|
+
const EQUIPMENT_ACTION_PATHS = new Set([
|
|
7
|
+
'/kcsapi/api_req_kaisou/slotset',
|
|
8
|
+
'/kcsapi/api_req_kaisou/slotset_ex',
|
|
9
|
+
'/kcsapi/api_req_kaisou/slot_deprive',
|
|
10
|
+
'/kcsapi/api_req_kaisou/unsetslot_all',
|
|
11
|
+
])
|
|
12
|
+
const FLEET_ACTION_PATH = '/kcsapi/api_req_hensei/change'
|
|
6
13
|
const BATTLE_RESULT_PATHS = new Set([
|
|
7
14
|
'/kcsapi/api_req_practice/battle_result',
|
|
8
15
|
'/kcsapi/api_req_sortie/battleresult',
|
|
@@ -37,6 +44,10 @@ function createPoiTelemetry(options = {}) {
|
|
|
37
44
|
let questList = null
|
|
38
45
|
let questActionGeneration = 0
|
|
39
46
|
let questAction = null
|
|
47
|
+
let equipmentActionGeneration = 0
|
|
48
|
+
let equipmentAction = null
|
|
49
|
+
let fleetActionGeneration = 0
|
|
50
|
+
let fleetAction = null
|
|
40
51
|
let battleGeneration = 0
|
|
41
52
|
let battleTelemetry = null
|
|
42
53
|
|
|
@@ -52,6 +63,14 @@ function createPoiTelemetry(options = {}) {
|
|
|
52
63
|
captureQuestAction(detail)
|
|
53
64
|
return
|
|
54
65
|
}
|
|
66
|
+
if (EQUIPMENT_ACTION_PATHS.has(detail.path)) {
|
|
67
|
+
captureEquipmentAction(detail)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (detail.path === FLEET_ACTION_PATH) {
|
|
71
|
+
captureFleetAction(detail)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
55
74
|
if (BATTLE_RESULT_PATHS.has(detail.path)) {
|
|
56
75
|
captureBattleResult(detail)
|
|
57
76
|
return
|
|
@@ -113,6 +132,40 @@ function createPoiTelemetry(options = {}) {
|
|
|
113
132
|
}
|
|
114
133
|
}
|
|
115
134
|
|
|
135
|
+
function captureEquipmentAction(detail) {
|
|
136
|
+
const apiResult = equipmentApiResult(detail)
|
|
137
|
+
if (apiResult === 0) return
|
|
138
|
+
const postBody = normalizeEquipmentPostBody(detail.path, detail.postBody)
|
|
139
|
+
if (!postBody) return
|
|
140
|
+
|
|
141
|
+
equipmentActionGeneration += 1
|
|
142
|
+
equipmentAction = {
|
|
143
|
+
available: true,
|
|
144
|
+
generation: equipmentActionGeneration,
|
|
145
|
+
capturedAt: now().toISOString(),
|
|
146
|
+
path: detail.path,
|
|
147
|
+
apiResult,
|
|
148
|
+
postBody,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function captureFleetAction(detail) {
|
|
153
|
+
const apiResult = equipmentApiResult(detail)
|
|
154
|
+
if (apiResult === 0) return
|
|
155
|
+
const postBody = normalizeFleetPostBody(detail.postBody)
|
|
156
|
+
if (!postBody) return
|
|
157
|
+
|
|
158
|
+
fleetActionGeneration += 1
|
|
159
|
+
fleetAction = {
|
|
160
|
+
available: true,
|
|
161
|
+
generation: fleetActionGeneration,
|
|
162
|
+
capturedAt: now().toISOString(),
|
|
163
|
+
path: detail.path,
|
|
164
|
+
apiResult,
|
|
165
|
+
postBody,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
116
169
|
function captureBattlePacket(detail) {
|
|
117
170
|
if (!battleTelemetry || battleTelemetry.status === 'settled') {
|
|
118
171
|
battleGeneration += 1
|
|
@@ -175,12 +228,101 @@ function createPoiTelemetry(options = {}) {
|
|
|
175
228
|
getQuestAction() {
|
|
176
229
|
return questAction || { available: false, generation: 0 }
|
|
177
230
|
},
|
|
231
|
+
getEquipmentAction() {
|
|
232
|
+
return equipmentAction || { available: false, generation: 0 }
|
|
233
|
+
},
|
|
234
|
+
getFleetAction() {
|
|
235
|
+
return fleetAction || { available: false, generation: 0 }
|
|
236
|
+
},
|
|
178
237
|
getBattleTelemetry() {
|
|
179
238
|
return battleTelemetry || { available: false, generation: 0 }
|
|
180
239
|
},
|
|
181
240
|
}
|
|
182
241
|
}
|
|
183
242
|
|
|
243
|
+
function normalizeFleetPostBody(postBody) {
|
|
244
|
+
if (!postBody || typeof postBody !== 'object') return null
|
|
245
|
+
const fleetId = boundedInteger(postBody.api_id, 1, 4)
|
|
246
|
+
const shipIndex = boundedInteger(postBody.api_ship_idx, 0, 5)
|
|
247
|
+
const shipId = boundedInteger(postBody.api_ship_id, -1)
|
|
248
|
+
if (fleetId == null || shipIndex == null || shipId == null) return null
|
|
249
|
+
return {
|
|
250
|
+
api_id: fleetId,
|
|
251
|
+
api_ship_idx: shipIndex,
|
|
252
|
+
api_ship_id: shipId,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeEquipmentPostBody(path, postBody) {
|
|
257
|
+
if (!postBody || typeof postBody !== 'object') return null
|
|
258
|
+
if (path === '/kcsapi/api_req_kaisou/slotset') {
|
|
259
|
+
const shipId = positiveIntegerOrNull(postBody.api_id)
|
|
260
|
+
const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 4)
|
|
261
|
+
const itemId = boundedInteger(postBody.api_item_id, -1)
|
|
262
|
+
if (shipId == null || slotIndex == null || itemId == null) return null
|
|
263
|
+
return {
|
|
264
|
+
api_id: shipId,
|
|
265
|
+
api_slot_idx: slotIndex,
|
|
266
|
+
api_item_id: itemId,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (path === '/kcsapi/api_req_kaisou/slotset_ex') {
|
|
270
|
+
const shipId = positiveIntegerOrNull(postBody.api_id)
|
|
271
|
+
const itemId = boundedInteger(postBody.api_item_id, -1)
|
|
272
|
+
if (shipId == null || itemId == null) return null
|
|
273
|
+
return {
|
|
274
|
+
api_id: shipId,
|
|
275
|
+
api_item_id: itemId,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (path === '/kcsapi/api_req_kaisou/slot_deprive') {
|
|
279
|
+
const setShip = positiveIntegerOrNull(postBody.api_set_ship)
|
|
280
|
+
const unsetShip = positiveIntegerOrNull(postBody.api_unset_ship)
|
|
281
|
+
const setIndex = boundedInteger(postBody.api_set_idx, -1, 4)
|
|
282
|
+
const unsetIndex = boundedInteger(postBody.api_unset_idx, -1, 4)
|
|
283
|
+
if (
|
|
284
|
+
setShip == null ||
|
|
285
|
+
unsetShip == null ||
|
|
286
|
+
setIndex == null ||
|
|
287
|
+
unsetIndex == null
|
|
288
|
+
) {
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
api_set_ship: setShip,
|
|
293
|
+
api_unset_ship: unsetShip,
|
|
294
|
+
api_set_idx: setIndex,
|
|
295
|
+
api_unset_idx: unsetIndex,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (path === '/kcsapi/api_req_kaisou/unsetslot_all') {
|
|
299
|
+
const shipId = positiveIntegerOrNull(postBody.api_id)
|
|
300
|
+
return shipId == null ? null : { api_id: shipId }
|
|
301
|
+
}
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function equipmentApiResult(detail) {
|
|
306
|
+
const candidates = [
|
|
307
|
+
detail.apiResult,
|
|
308
|
+
detail.api_result,
|
|
309
|
+
detail.result,
|
|
310
|
+
detail.body && detail.body.api_result,
|
|
311
|
+
]
|
|
312
|
+
for (const candidate of candidates) {
|
|
313
|
+
const value = toInteger(candidate)
|
|
314
|
+
if (value != null) return value === 1 ? 1 : 0
|
|
315
|
+
}
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function boundedInteger(value, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
320
|
+
const parsed = toInteger(value)
|
|
321
|
+
return parsed != null && parsed >= minimum && parsed <= maximum
|
|
322
|
+
? parsed
|
|
323
|
+
: null
|
|
324
|
+
}
|
|
325
|
+
|
|
184
326
|
function toInteger(value, fallback = null) {
|
|
185
327
|
if (value == null || value === '') return fallback
|
|
186
328
|
const parsed = Number(value)
|