poi-plugin-mcp 0.2.9 → 0.2.11
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 +1 -0
- package/lib/bridge-controller.js +1 -0
- package/lib/poi-http-bridge.js +60 -2
- package/lib/poi-telemetry.js +103 -1
- 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,7 @@ const telemetry = createPoiTelemetry()
|
|
|
6
6
|
const controller = createBridgeController({
|
|
7
7
|
getQuestList: telemetry.getQuestList,
|
|
8
8
|
getQuestAction: telemetry.getQuestAction,
|
|
9
|
+
getEquipmentAction: telemetry.getEquipmentAction,
|
|
9
10
|
getBattleTelemetry: telemetry.getBattleTelemetry,
|
|
10
11
|
})
|
|
11
12
|
const settingsClass = createSettingsClass(controller)
|
package/lib/bridge-controller.js
CHANGED
|
@@ -32,6 +32,7 @@ function createBridgeController(options = {}) {
|
|
|
32
32
|
performInput: options.performInput,
|
|
33
33
|
getQuestList: options.getQuestList,
|
|
34
34
|
getQuestAction: options.getQuestAction,
|
|
35
|
+
getEquipmentAction: options.getEquipmentAction,
|
|
35
36
|
getBattleTelemetry: options.getBattleTelemetry,
|
|
36
37
|
inputEnabled: settings.inputEnabled,
|
|
37
38
|
inputToken,
|
package/lib/poi-http-bridge.js
CHANGED
|
@@ -15,18 +15,28 @@ 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 }))
|
|
30
40
|
const getBattleTelemetry = options.getBattleTelemetry ||
|
|
31
41
|
(() => ({ available: false, generation: 0 }))
|
|
32
42
|
const inputEnabled = options.inputEnabled === true
|
|
@@ -242,6 +252,11 @@ function createPoiDataBridge(options = {}) {
|
|
|
242
252
|
return
|
|
243
253
|
}
|
|
244
254
|
|
|
255
|
+
if (endpoint === '/equipment-action') {
|
|
256
|
+
sendJson(res, 200, getEquipmentAction())
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
245
260
|
const store = readStore()
|
|
246
261
|
const info = store.info
|
|
247
262
|
|
|
@@ -274,7 +289,7 @@ function createPoiDataBridge(options = {}) {
|
|
|
274
289
|
sendJson(res, 200, extractNames(store))
|
|
275
290
|
break
|
|
276
291
|
case '/master':
|
|
277
|
-
sendJson(res, 200, extractMasterData(store))
|
|
292
|
+
sendJson(res, 200, extractMasterData(store, masterFile))
|
|
278
293
|
break
|
|
279
294
|
case '/event':
|
|
280
295
|
sendJson(res, 200, extractEventData(store))
|
|
@@ -906,7 +921,7 @@ function collectSimpleNames(source, target) {
|
|
|
906
921
|
}
|
|
907
922
|
}
|
|
908
923
|
|
|
909
|
-
function extractMasterData(store) {
|
|
924
|
+
function extractMasterData(store, masterFile = DEFAULT_MASTER_FILE) {
|
|
910
925
|
const constants = store.const || {}
|
|
911
926
|
|
|
912
927
|
return {
|
|
@@ -920,9 +935,52 @@ function extractMasterData(store) {
|
|
|
920
935
|
constants.$slotItemTypes ||
|
|
921
936
|
{},
|
|
922
937
|
missions: constants.$missions || {},
|
|
938
|
+
equipmentRules: readEquipmentRules(masterFile),
|
|
923
939
|
}
|
|
924
940
|
}
|
|
925
941
|
|
|
942
|
+
function readEquipmentRules(masterFile) {
|
|
943
|
+
try {
|
|
944
|
+
const stat = fs.statSync(masterFile)
|
|
945
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MASTER_FILE_LIMIT) {
|
|
946
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
947
|
+
}
|
|
948
|
+
const master = JSON.parse(fs.readFileSync(masterFile, 'utf8'))
|
|
949
|
+
const equipmentShip = objectOrEmpty(master.api_mst_equip_ship)
|
|
950
|
+
const equipmentExslotTypes = Array.isArray(master.api_mst_equip_exslot)
|
|
951
|
+
? master.api_mst_equip_exslot.filter(Number.isInteger)
|
|
952
|
+
: []
|
|
953
|
+
const equipmentExslotShip = objectOrEmpty(
|
|
954
|
+
master.api_mst_equip_exslot_ship,
|
|
955
|
+
)
|
|
956
|
+
const equipmentLimitExslot = objectOrEmpty(
|
|
957
|
+
master.api_mst_equip_limit_exslot,
|
|
958
|
+
)
|
|
959
|
+
if (
|
|
960
|
+
Object.keys(equipmentShip).length === 0 ||
|
|
961
|
+
equipmentExslotTypes.length === 0
|
|
962
|
+
) {
|
|
963
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
available: true,
|
|
967
|
+
source: 'navy-album-master-cache',
|
|
968
|
+
equipmentShip,
|
|
969
|
+
equipmentExslotTypes,
|
|
970
|
+
equipmentExslotShip,
|
|
971
|
+
equipmentLimitExslot,
|
|
972
|
+
}
|
|
973
|
+
} catch (_) {
|
|
974
|
+
return { available: false, source: 'navy-album-master-cache' }
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function objectOrEmpty(value) {
|
|
979
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
980
|
+
? value
|
|
981
|
+
: {}
|
|
982
|
+
}
|
|
983
|
+
|
|
926
984
|
function extractEventData(store) {
|
|
927
985
|
const tags = extractShipTags(store)
|
|
928
986
|
const ships = {}
|
package/lib/poi-telemetry.js
CHANGED
|
@@ -3,6 +3,12 @@ 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
|
+
])
|
|
6
12
|
const BATTLE_RESULT_PATHS = new Set([
|
|
7
13
|
'/kcsapi/api_req_practice/battle_result',
|
|
8
14
|
'/kcsapi/api_req_sortie/battleresult',
|
|
@@ -37,6 +43,8 @@ function createPoiTelemetry(options = {}) {
|
|
|
37
43
|
let questList = null
|
|
38
44
|
let questActionGeneration = 0
|
|
39
45
|
let questAction = null
|
|
46
|
+
let equipmentActionGeneration = 0
|
|
47
|
+
let equipmentAction = null
|
|
40
48
|
let battleGeneration = 0
|
|
41
49
|
let battleTelemetry = null
|
|
42
50
|
|
|
@@ -52,6 +60,10 @@ function createPoiTelemetry(options = {}) {
|
|
|
52
60
|
captureQuestAction(detail)
|
|
53
61
|
return
|
|
54
62
|
}
|
|
63
|
+
if (EQUIPMENT_ACTION_PATHS.has(detail.path)) {
|
|
64
|
+
captureEquipmentAction(detail)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
55
67
|
if (BATTLE_RESULT_PATHS.has(detail.path)) {
|
|
56
68
|
captureBattleResult(detail)
|
|
57
69
|
return
|
|
@@ -79,7 +91,7 @@ function createPoiTelemetry(options = {}) {
|
|
|
79
91
|
if (!quest || typeof quest !== 'object' || !Number.isInteger(quest.api_no)) {
|
|
80
92
|
return []
|
|
81
93
|
}
|
|
82
|
-
return [{ ...quest,
|
|
94
|
+
return [{ ...quest, listIndex: index + 1 }]
|
|
83
95
|
})
|
|
84
96
|
|
|
85
97
|
questGeneration += 1
|
|
@@ -113,6 +125,23 @@ function createPoiTelemetry(options = {}) {
|
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
function captureEquipmentAction(detail) {
|
|
129
|
+
const apiResult = equipmentApiResult(detail)
|
|
130
|
+
if (apiResult === 0) return
|
|
131
|
+
const postBody = normalizeEquipmentPostBody(detail.path, detail.postBody)
|
|
132
|
+
if (!postBody) return
|
|
133
|
+
|
|
134
|
+
equipmentActionGeneration += 1
|
|
135
|
+
equipmentAction = {
|
|
136
|
+
available: true,
|
|
137
|
+
generation: equipmentActionGeneration,
|
|
138
|
+
capturedAt: now().toISOString(),
|
|
139
|
+
path: detail.path,
|
|
140
|
+
apiResult,
|
|
141
|
+
postBody,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
function captureBattlePacket(detail) {
|
|
117
146
|
if (!battleTelemetry || battleTelemetry.status === 'settled') {
|
|
118
147
|
battleGeneration += 1
|
|
@@ -175,12 +204,85 @@ function createPoiTelemetry(options = {}) {
|
|
|
175
204
|
getQuestAction() {
|
|
176
205
|
return questAction || { available: false, generation: 0 }
|
|
177
206
|
},
|
|
207
|
+
getEquipmentAction() {
|
|
208
|
+
return equipmentAction || { available: false, generation: 0 }
|
|
209
|
+
},
|
|
178
210
|
getBattleTelemetry() {
|
|
179
211
|
return battleTelemetry || { available: false, generation: 0 }
|
|
180
212
|
},
|
|
181
213
|
}
|
|
182
214
|
}
|
|
183
215
|
|
|
216
|
+
function normalizeEquipmentPostBody(path, postBody) {
|
|
217
|
+
if (!postBody || typeof postBody !== 'object') return null
|
|
218
|
+
if (path === '/kcsapi/api_req_kaisou/slotset') {
|
|
219
|
+
const shipId = positiveIntegerOrNull(postBody.api_id)
|
|
220
|
+
const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 4)
|
|
221
|
+
const itemId = boundedInteger(postBody.api_item_id, -1)
|
|
222
|
+
if (shipId == null || slotIndex == null || itemId == null) return null
|
|
223
|
+
return {
|
|
224
|
+
api_id: shipId,
|
|
225
|
+
api_slot_idx: slotIndex,
|
|
226
|
+
api_item_id: itemId,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (path === '/kcsapi/api_req_kaisou/slotset_ex') {
|
|
230
|
+
const shipId = positiveIntegerOrNull(postBody.api_id)
|
|
231
|
+
const itemId = boundedInteger(postBody.api_item_id, -1)
|
|
232
|
+
if (shipId == null || itemId == null) return null
|
|
233
|
+
return {
|
|
234
|
+
api_id: shipId,
|
|
235
|
+
api_item_id: itemId,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (path === '/kcsapi/api_req_kaisou/slot_deprive') {
|
|
239
|
+
const setShip = positiveIntegerOrNull(postBody.api_set_ship)
|
|
240
|
+
const unsetShip = positiveIntegerOrNull(postBody.api_unset_ship)
|
|
241
|
+
const setIndex = boundedInteger(postBody.api_set_idx, -1, 4)
|
|
242
|
+
const unsetIndex = boundedInteger(postBody.api_unset_idx, -1, 4)
|
|
243
|
+
if (
|
|
244
|
+
setShip == null ||
|
|
245
|
+
unsetShip == null ||
|
|
246
|
+
setIndex == null ||
|
|
247
|
+
unsetIndex == null
|
|
248
|
+
) {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
api_set_ship: setShip,
|
|
253
|
+
api_unset_ship: unsetShip,
|
|
254
|
+
api_set_idx: setIndex,
|
|
255
|
+
api_unset_idx: unsetIndex,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (path === '/kcsapi/api_req_kaisou/unsetslot_all') {
|
|
259
|
+
const shipId = positiveIntegerOrNull(postBody.api_id)
|
|
260
|
+
return shipId == null ? null : { api_id: shipId }
|
|
261
|
+
}
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function equipmentApiResult(detail) {
|
|
266
|
+
const candidates = [
|
|
267
|
+
detail.apiResult,
|
|
268
|
+
detail.api_result,
|
|
269
|
+
detail.result,
|
|
270
|
+
detail.body && detail.body.api_result,
|
|
271
|
+
]
|
|
272
|
+
for (const candidate of candidates) {
|
|
273
|
+
const value = toInteger(candidate)
|
|
274
|
+
if (value != null) return value === 1 ? 1 : 0
|
|
275
|
+
}
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function boundedInteger(value, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
280
|
+
const parsed = toInteger(value)
|
|
281
|
+
return parsed != null && parsed >= minimum && parsed <= maximum
|
|
282
|
+
? parsed
|
|
283
|
+
: null
|
|
284
|
+
}
|
|
285
|
+
|
|
184
286
|
function toInteger(value, fallback = null) {
|
|
185
287
|
if (value == null || value === '') return fallback
|
|
186
288
|
const parsed = Number(value)
|