poi-plugin-mcp 0.2.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/README.md +76 -0
- package/index.js +25 -0
- package/lib/bridge-controller.js +118 -0
- package/lib/poi-http-bridge.js +669 -0
- package/lib/settings-view.js +184 -0
- package/lib/settings.js +51 -0
- package/mcp-server.js +397 -0
- package/package.json +32 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const http = require('http')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PORT = 17777
|
|
7
|
+
const DEFAULT_PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
|
|
8
|
+
const DEFAULT_PLANNER_FILE = path.join(
|
|
9
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
10
|
+
'poi',
|
|
11
|
+
'poi-plugin-ship-info.json',
|
|
12
|
+
)
|
|
13
|
+
const JSONRPC_VERSION = '2.0'
|
|
14
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05'
|
|
15
|
+
|
|
16
|
+
function createPoiDataBridge(options = {}) {
|
|
17
|
+
const getStore = options.getStore || defaultGetStore
|
|
18
|
+
const configuredPort = options.port == null ? DEFAULT_PORT : options.port
|
|
19
|
+
const portFile = options.portFile || DEFAULT_PORT_FILE
|
|
20
|
+
const plannerFile = options.plannerFile || DEFAULT_PLANNER_FILE
|
|
21
|
+
const logger = options.logger || console
|
|
22
|
+
|
|
23
|
+
let server = null
|
|
24
|
+
let actualPort = 0
|
|
25
|
+
|
|
26
|
+
function readStore() {
|
|
27
|
+
const store = getStore()
|
|
28
|
+
if (!store || !store.info) {
|
|
29
|
+
throw new Error('POI store not ready. Enter the game first.')
|
|
30
|
+
}
|
|
31
|
+
return store
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendJson(res, statusCode, data) {
|
|
35
|
+
res.writeHead(statusCode, {
|
|
36
|
+
'Access-Control-Allow-Origin': '*',
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
})
|
|
39
|
+
res.end(JSON.stringify(data))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleMcpRequest(req, res) {
|
|
43
|
+
if (req.method !== 'POST') {
|
|
44
|
+
sendJson(res, 405, { error: 'MCP endpoint only accepts POST requests.' })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
readRequestBody(req)
|
|
49
|
+
.then((body) => {
|
|
50
|
+
const message = JSON.parse(body || '{}')
|
|
51
|
+
const response = handleMcpMessage(message, readStore, plannerFile)
|
|
52
|
+
|
|
53
|
+
if (response == null) {
|
|
54
|
+
res.writeHead(202, {
|
|
55
|
+
'Access-Control-Allow-Origin': '*',
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
})
|
|
58
|
+
res.end('')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
sendJson(res, 200, response)
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
sendJson(res, 200, jsonRpcError(null, -32700, error.message))
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleRequest(req, res) {
|
|
70
|
+
try {
|
|
71
|
+
if (req.url === '/shutdown') {
|
|
72
|
+
sendJson(res, 200, { status: 'shutting down' })
|
|
73
|
+
stop()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const endpoint = new URL(req.url, `http://127.0.0.1:${actualPort || configuredPort}`).pathname
|
|
78
|
+
|
|
79
|
+
if (endpoint === '/health') {
|
|
80
|
+
sendJson(res, 200, { status: 'ok' })
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (endpoint === '/mcp') {
|
|
85
|
+
handleMcpRequest(req, res)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const store = readStore()
|
|
90
|
+
const info = store.info
|
|
91
|
+
|
|
92
|
+
switch (endpoint) {
|
|
93
|
+
case '/basic':
|
|
94
|
+
sendJson(res, 200, info.basic || {})
|
|
95
|
+
break
|
|
96
|
+
case '/fleets':
|
|
97
|
+
sendJson(res, 200, info.fleets || [])
|
|
98
|
+
break
|
|
99
|
+
case '/ships':
|
|
100
|
+
sendJson(res, 200, info.ships || {})
|
|
101
|
+
break
|
|
102
|
+
case '/equipment':
|
|
103
|
+
sendJson(res, 200, info.equips || {})
|
|
104
|
+
break
|
|
105
|
+
case '/resources':
|
|
106
|
+
sendJson(res, 200, info.resources || [])
|
|
107
|
+
break
|
|
108
|
+
case '/quests':
|
|
109
|
+
sendJson(res, 200, {
|
|
110
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
111
|
+
records: (info.quests && info.quests.records) || {},
|
|
112
|
+
})
|
|
113
|
+
break
|
|
114
|
+
case '/airbase':
|
|
115
|
+
sendJson(res, 200, info.airbase || [])
|
|
116
|
+
break
|
|
117
|
+
case '/names':
|
|
118
|
+
sendJson(res, 200, extractNames(store))
|
|
119
|
+
break
|
|
120
|
+
case '/master':
|
|
121
|
+
sendJson(res, 200, extractMasterData(store))
|
|
122
|
+
break
|
|
123
|
+
case '/event':
|
|
124
|
+
sendJson(res, 200, extractEventData(store))
|
|
125
|
+
break
|
|
126
|
+
case '/planner':
|
|
127
|
+
sendJson(res, 200, extractPlannerData(store, plannerFile))
|
|
128
|
+
break
|
|
129
|
+
case '/all':
|
|
130
|
+
sendJson(res, 200, {
|
|
131
|
+
basic: info.basic || {},
|
|
132
|
+
fleets: info.fleets || [],
|
|
133
|
+
ships: info.ships || {},
|
|
134
|
+
equipment: info.equips || {},
|
|
135
|
+
resources: info.resources || [],
|
|
136
|
+
quests: {
|
|
137
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
138
|
+
records: (info.quests && info.quests.records) || {},
|
|
139
|
+
},
|
|
140
|
+
airbase: info.airbase || [],
|
|
141
|
+
names: extractNames(store),
|
|
142
|
+
})
|
|
143
|
+
break
|
|
144
|
+
default:
|
|
145
|
+
sendJson(res, 404, { error: `Unknown endpoint: ${endpoint}` })
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
sendJson(res, 503, { error: error.message })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function start() {
|
|
153
|
+
if (server) return Promise.resolve()
|
|
154
|
+
|
|
155
|
+
fs.mkdirSync(path.dirname(portFile), { recursive: true })
|
|
156
|
+
server = http.createServer(handleRequest)
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const onError = (error) => {
|
|
160
|
+
cleanupPortFile(portFile)
|
|
161
|
+
server = null
|
|
162
|
+
reject(error)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
server.once('error', onError)
|
|
166
|
+
server.listen(configuredPort, '127.0.0.1', () => {
|
|
167
|
+
server.removeListener('error', onError)
|
|
168
|
+
actualPort = server.address().port
|
|
169
|
+
fs.writeFileSync(portFile, String(actualPort), 'utf8')
|
|
170
|
+
logger.log(`[poi-plugin-mcp] HTTP API started on http://127.0.0.1:${actualPort}`)
|
|
171
|
+
resolve()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function stop() {
|
|
177
|
+
if (!server) {
|
|
178
|
+
cleanupPortFile(portFile)
|
|
179
|
+
actualPort = 0
|
|
180
|
+
return Promise.resolve()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const closingServer = server
|
|
184
|
+
server = null
|
|
185
|
+
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
closingServer.close(() => {
|
|
188
|
+
cleanupPortFile(portFile)
|
|
189
|
+
actualPort = 0
|
|
190
|
+
resolve()
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
start,
|
|
197
|
+
stop,
|
|
198
|
+
getPort() {
|
|
199
|
+
return actualPort
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readRequestBody(req) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
let body = ''
|
|
207
|
+
req.setEncoding('utf8')
|
|
208
|
+
req.on('data', (chunk) => {
|
|
209
|
+
body += chunk
|
|
210
|
+
if (body.length > 1024 * 1024) {
|
|
211
|
+
reject(new Error('MCP request body is too large.'))
|
|
212
|
+
req.destroy()
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
req.on('end', () => resolve(body))
|
|
216
|
+
req.on('error', reject)
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleMcpMessage(message, readStore, plannerFile) {
|
|
221
|
+
const { id, method, params } = message || {}
|
|
222
|
+
|
|
223
|
+
switch (method) {
|
|
224
|
+
case 'initialize':
|
|
225
|
+
return jsonRpcResult(id, {
|
|
226
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
227
|
+
capabilities: {
|
|
228
|
+
resources: { subscribe: false },
|
|
229
|
+
tools: {},
|
|
230
|
+
},
|
|
231
|
+
serverInfo: { name: 'poi-plugin-mcp', version: '0.2.0' },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
case 'notifications/initialized':
|
|
235
|
+
case 'notifications/cancelled':
|
|
236
|
+
return null
|
|
237
|
+
|
|
238
|
+
case 'ping':
|
|
239
|
+
return jsonRpcResult(id, {})
|
|
240
|
+
|
|
241
|
+
case 'resources/list':
|
|
242
|
+
return jsonRpcResult(id, {
|
|
243
|
+
resources: MCP_RESOURCE_ENDPOINTS.map(({ uri }) => ({
|
|
244
|
+
uri,
|
|
245
|
+
name: uri.replace('poi://', ''),
|
|
246
|
+
mimeType: 'application/json',
|
|
247
|
+
})),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
case 'resources/read': {
|
|
251
|
+
const uri = params && params.uri
|
|
252
|
+
const endpoint = MCP_RESOURCE_ENDPOINTS.find((item) => item.uri === uri)
|
|
253
|
+
if (!endpoint) return jsonRpcError(id, -32602, `Unknown resource: ${uri}`)
|
|
254
|
+
const data = readBridgeData(endpoint.path, readStore, plannerFile)
|
|
255
|
+
return jsonRpcResult(id, {
|
|
256
|
+
contents: [{
|
|
257
|
+
uri,
|
|
258
|
+
mimeType: 'application/json',
|
|
259
|
+
text: JSON.stringify(data, null, 2),
|
|
260
|
+
}],
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'tools/list':
|
|
265
|
+
return jsonRpcResult(id, { tools: MCP_TOOLS })
|
|
266
|
+
|
|
267
|
+
case 'tools/call': {
|
|
268
|
+
const toolName = params && params.name
|
|
269
|
+
const toolArgs = (params && params.arguments) || {}
|
|
270
|
+
const result = callMcpTool(toolName, toolArgs, readStore, plannerFile)
|
|
271
|
+
if (result.error) return jsonRpcError(id, -32602, result.error)
|
|
272
|
+
return jsonRpcResult(id, {
|
|
273
|
+
content: [{ type: 'text', text: JSON.stringify(result.value, null, 2) }],
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
default:
|
|
278
|
+
return jsonRpcError(id, -32601, `Unknown method: ${method}`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const MCP_RESOURCE_ENDPOINTS = [
|
|
283
|
+
{ uri: 'poi://basic', path: '/basic' },
|
|
284
|
+
{ uri: 'poi://fleets', path: '/fleets' },
|
|
285
|
+
{ uri: 'poi://ships', path: '/ships' },
|
|
286
|
+
{ uri: 'poi://equipment', path: '/equipment' },
|
|
287
|
+
{ uri: 'poi://resources', path: '/resources' },
|
|
288
|
+
{ uri: 'poi://quests', path: '/quests' },
|
|
289
|
+
{ uri: 'poi://airbase', path: '/airbase' },
|
|
290
|
+
{ uri: 'poi://names', path: '/names' },
|
|
291
|
+
{ uri: 'poi://master', path: '/master' },
|
|
292
|
+
{ uri: 'poi://event', path: '/event' },
|
|
293
|
+
{ uri: 'poi://planner', path: '/planner' },
|
|
294
|
+
{ uri: 'poi://all', path: '/all' },
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
const MCP_TOOLS = [
|
|
298
|
+
{
|
|
299
|
+
name: 'get_fleet_status',
|
|
300
|
+
description: 'Get one owned fleet with ship and equipped item instance details.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: { fleetId: { type: 'number', description: 'Fleet number, 1-4.' } },
|
|
304
|
+
required: ['fleetId'],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'search_ships',
|
|
309
|
+
description: 'Search owned ship instances by level and morale.',
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
minLevel: { type: 'number' },
|
|
314
|
+
maxLevel: { type: 'number' },
|
|
315
|
+
minMorale: { type: 'number' },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'search_equipment',
|
|
321
|
+
description: 'Search owned equipment instances by improvement level.',
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
minLevel: { type: 'number', description: 'Minimum improvement level.' },
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: 'get_resources',
|
|
331
|
+
description: 'Get current account resource array.',
|
|
332
|
+
inputSchema: { type: 'object', properties: {} },
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'get_all',
|
|
336
|
+
description: 'Get account basics, fleets, ships, equipment, resources, quests, airbase, and names.',
|
|
337
|
+
inputSchema: { type: 'object', properties: {} },
|
|
338
|
+
},
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
function callMcpTool(toolName, args, readStore, plannerFile) {
|
|
342
|
+
switch (toolName) {
|
|
343
|
+
case 'get_fleet_status':
|
|
344
|
+
return { value: buildFleetStatus(args || {}, readStore) }
|
|
345
|
+
case 'search_ships':
|
|
346
|
+
return { value: searchShips(args || {}, readStore) }
|
|
347
|
+
case 'search_equipment':
|
|
348
|
+
return { value: searchEquipment(args || {}, readStore) }
|
|
349
|
+
case 'get_resources':
|
|
350
|
+
return { value: readBridgeData('/resources', readStore, plannerFile) }
|
|
351
|
+
case 'get_all':
|
|
352
|
+
return { value: readBridgeData('/all', readStore, plannerFile) }
|
|
353
|
+
default:
|
|
354
|
+
return { error: `Unknown tool: ${toolName}` }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function readBridgeData(endpoint, readStore, plannerFile) {
|
|
359
|
+
const store = readStore()
|
|
360
|
+
const info = store.info
|
|
361
|
+
|
|
362
|
+
switch (endpoint) {
|
|
363
|
+
case '/basic':
|
|
364
|
+
return info.basic || {}
|
|
365
|
+
case '/fleets':
|
|
366
|
+
return info.fleets || []
|
|
367
|
+
case '/ships':
|
|
368
|
+
return info.ships || {}
|
|
369
|
+
case '/equipment':
|
|
370
|
+
return info.equips || {}
|
|
371
|
+
case '/resources':
|
|
372
|
+
return info.resources || []
|
|
373
|
+
case '/quests':
|
|
374
|
+
return {
|
|
375
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
376
|
+
records: (info.quests && info.quests.records) || {},
|
|
377
|
+
}
|
|
378
|
+
case '/airbase':
|
|
379
|
+
return info.airbase || []
|
|
380
|
+
case '/names':
|
|
381
|
+
return extractNames(store)
|
|
382
|
+
case '/master':
|
|
383
|
+
return extractMasterData(store)
|
|
384
|
+
case '/event':
|
|
385
|
+
return extractEventData(store)
|
|
386
|
+
case '/planner':
|
|
387
|
+
return extractPlannerData(store, plannerFile)
|
|
388
|
+
case '/all':
|
|
389
|
+
return {
|
|
390
|
+
basic: info.basic || {},
|
|
391
|
+
fleets: info.fleets || [],
|
|
392
|
+
ships: info.ships || {},
|
|
393
|
+
equipment: info.equips || {},
|
|
394
|
+
resources: info.resources || [],
|
|
395
|
+
quests: {
|
|
396
|
+
activeQuests: (info.quests && info.quests.activeQuests) || {},
|
|
397
|
+
records: (info.quests && info.quests.records) || {},
|
|
398
|
+
},
|
|
399
|
+
airbase: info.airbase || [],
|
|
400
|
+
names: extractNames(store),
|
|
401
|
+
}
|
|
402
|
+
default:
|
|
403
|
+
throw new Error(`Unknown endpoint: ${endpoint}`)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function buildFleetStatus(args, readStore) {
|
|
408
|
+
const fleetId = Number(args.fleetId)
|
|
409
|
+
const store = readStore()
|
|
410
|
+
const info = store.info
|
|
411
|
+
const fleets = Array.isArray(info.fleets) ? info.fleets : []
|
|
412
|
+
const fleet = fleets[fleetId - 1]
|
|
413
|
+
if (!fleet) return { error: `Fleet #${args.fleetId} not found` }
|
|
414
|
+
|
|
415
|
+
const ships = info.ships || {}
|
|
416
|
+
const equips = info.equips || {}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
id: fleet.api_id,
|
|
420
|
+
name: fleet.api_name,
|
|
421
|
+
mission: fleet.api_mission,
|
|
422
|
+
ships: (fleet.api_ship || []).filter((id) => id > 0).map((shipId) => {
|
|
423
|
+
const ship = ships[shipId]
|
|
424
|
+
if (!ship) return { id: shipId }
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
id: ship.api_id,
|
|
428
|
+
shipId: ship.api_ship_id,
|
|
429
|
+
level: ship.api_lv,
|
|
430
|
+
hp: `${ship.api_nowhp}/${ship.api_maxhp}`,
|
|
431
|
+
morale: ship.api_cond,
|
|
432
|
+
locked: ship.api_locked,
|
|
433
|
+
slotItems: (ship.api_slot || []).filter((equipId) => equipId > 0).map((equipId) => {
|
|
434
|
+
const equip = equips[equipId]
|
|
435
|
+
return equip ? {
|
|
436
|
+
id: equip.api_id,
|
|
437
|
+
equipId: equip.api_slotitem_id,
|
|
438
|
+
level: equip.api_level || 0,
|
|
439
|
+
prof: equip.api_alv || 0,
|
|
440
|
+
} : null
|
|
441
|
+
}).filter(Boolean),
|
|
442
|
+
}
|
|
443
|
+
}),
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function searchShips(args, readStore) {
|
|
448
|
+
const store = readStore()
|
|
449
|
+
const ships = Object.values((store.info && store.info.ships) || {}).filter((ship) => {
|
|
450
|
+
if (!ship) return false
|
|
451
|
+
if (args.minLevel != null && ship.api_lv < Number(args.minLevel)) return false
|
|
452
|
+
if (args.maxLevel != null && ship.api_lv > Number(args.maxLevel)) return false
|
|
453
|
+
if (args.minMorale != null && ship.api_cond < Number(args.minMorale)) return false
|
|
454
|
+
return true
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
return { total: ships.length, ships }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function searchEquipment(args, readStore) {
|
|
461
|
+
const store = readStore()
|
|
462
|
+
const equipment = Object.values((store.info && store.info.equips) || {}).filter((equip) => {
|
|
463
|
+
if (!equip) return false
|
|
464
|
+
if (args.minLevel != null && (equip.api_level || 0) < Number(args.minLevel)) return false
|
|
465
|
+
return true
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
return { total: equipment.length, equipment }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function jsonRpcResult(id, result) {
|
|
472
|
+
return { jsonrpc: JSONRPC_VERSION, id, result }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function jsonRpcError(id, code, message) {
|
|
476
|
+
return { jsonrpc: JSONRPC_VERSION, id, error: { code, message } }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function defaultGetStore() {
|
|
480
|
+
if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
|
|
481
|
+
return window.getStore()
|
|
482
|
+
}
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function cleanupPortFile(portFile) {
|
|
487
|
+
try {
|
|
488
|
+
fs.unlinkSync(portFile)
|
|
489
|
+
} catch (_) {}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function extractNames(store) {
|
|
493
|
+
const result = { ships: {}, equipment: {}, missions: {} }
|
|
494
|
+
const constants = store.const || {}
|
|
495
|
+
|
|
496
|
+
collectApiNames(constants.$ships, result.ships)
|
|
497
|
+
collectApiNames(constants.$equips, result.equipment)
|
|
498
|
+
collectApiNames(constants.$missions, result.missions)
|
|
499
|
+
|
|
500
|
+
const wctf = store.wctf || {}
|
|
501
|
+
if (Object.keys(result.ships).length === 0) collectSimpleNames(wctf.ships, result.ships)
|
|
502
|
+
if (Object.keys(result.equipment).length === 0) collectSimpleNames(wctf.items, result.equipment)
|
|
503
|
+
|
|
504
|
+
return result
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function collectApiNames(source, target) {
|
|
508
|
+
if (!source || typeof source !== 'object') return
|
|
509
|
+
|
|
510
|
+
for (const [id, value] of Object.entries(source)) {
|
|
511
|
+
if (value && value.api_name) target[id] = value.api_name
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function collectSimpleNames(source, target) {
|
|
516
|
+
if (!source || typeof source !== 'object') return
|
|
517
|
+
|
|
518
|
+
for (const [id, value] of Object.entries(source)) {
|
|
519
|
+
if (value && value.name) target[id] = value.name
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function extractMasterData(store) {
|
|
524
|
+
const constants = store.const || {}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
ships: constants.$ships || {},
|
|
528
|
+
equipment: constants.$equips || {},
|
|
529
|
+
shipTypes: constants.$shipTypes || {},
|
|
530
|
+
equipmentTypes:
|
|
531
|
+
constants.$equipTypes ||
|
|
532
|
+
constants.$equipmentTypes ||
|
|
533
|
+
constants.$slotitemTypes ||
|
|
534
|
+
constants.$slotItemTypes ||
|
|
535
|
+
{},
|
|
536
|
+
missions: constants.$missions || {},
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function extractEventData(store) {
|
|
541
|
+
const tags = extractShipTags(store)
|
|
542
|
+
const ships = {}
|
|
543
|
+
|
|
544
|
+
for (const ship of Object.values((store.info && store.info.ships) || {})) {
|
|
545
|
+
if (!ship || typeof ship !== 'object') continue
|
|
546
|
+
const area = ship.api_sally_area || 0
|
|
547
|
+
const tag = tags[area - 1] || emptyTag(area)
|
|
548
|
+
|
|
549
|
+
ships[ship.api_id] = {
|
|
550
|
+
shipId: ship.api_id,
|
|
551
|
+
modelId: ship.api_ship_id,
|
|
552
|
+
area,
|
|
553
|
+
mapName: tag.mapName,
|
|
554
|
+
fleetName: tag.fleetName,
|
|
555
|
+
color: tag.color,
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
tags,
|
|
561
|
+
ships,
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function extractPlannerData(store, plannerFile = DEFAULT_PLANNER_FILE) {
|
|
566
|
+
const tags = extractShipTags(store)
|
|
567
|
+
const current = readPlannerCurrent(plannerFile)
|
|
568
|
+
const length = Math.max(tags.length, current.length)
|
|
569
|
+
const areas = []
|
|
570
|
+
const shipMap = {}
|
|
571
|
+
|
|
572
|
+
for (let index = 0; index < length; index += 1) {
|
|
573
|
+
const area = index + 1
|
|
574
|
+
const tag = tags[index] || emptyTag(area)
|
|
575
|
+
const shipIds = Array.isArray(current[index]) ? current[index] : []
|
|
576
|
+
|
|
577
|
+
areas.push({
|
|
578
|
+
area,
|
|
579
|
+
mapName: tag.mapName,
|
|
580
|
+
fleetName: tag.fleetName,
|
|
581
|
+
color: tag.color,
|
|
582
|
+
shipIds,
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
for (const shipId of shipIds) {
|
|
586
|
+
shipMap[shipId] = {
|
|
587
|
+
area,
|
|
588
|
+
mapName: tag.mapName,
|
|
589
|
+
fleetName: tag.fleetName,
|
|
590
|
+
color: tag.color,
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
areas,
|
|
597
|
+
shipMap,
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function readPlannerCurrent(plannerFile) {
|
|
602
|
+
try {
|
|
603
|
+
const data = JSON.parse(fs.readFileSync(plannerFile, 'utf8'))
|
|
604
|
+
if (Array.isArray(data.planner)) return data.planner
|
|
605
|
+
if (data.planner && Array.isArray(data.planner.current)) {
|
|
606
|
+
return data.planner.current
|
|
607
|
+
}
|
|
608
|
+
} catch (_) {}
|
|
609
|
+
|
|
610
|
+
return []
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function extractShipTags(store) {
|
|
614
|
+
const shiptag = (store.fcd && store.fcd.shiptag) || {}
|
|
615
|
+
const mapNames = Array.isArray(shiptag.mapname) ? shiptag.mapname : []
|
|
616
|
+
const fleetNames = selectFleetNames(shiptag.fleetname)
|
|
617
|
+
const colors = Array.isArray(shiptag.color) ? shiptag.color : []
|
|
618
|
+
|
|
619
|
+
return mapNames.map((mapName, index) => ({
|
|
620
|
+
area: index + 1,
|
|
621
|
+
mapName,
|
|
622
|
+
fleetName: fleetNames[index] || mapName,
|
|
623
|
+
color: colors[index] || '',
|
|
624
|
+
}))
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function selectFleetNames(fleetname) {
|
|
628
|
+
if (Array.isArray(fleetname)) return fleetname
|
|
629
|
+
if (!fleetname || typeof fleetname !== 'object') return []
|
|
630
|
+
|
|
631
|
+
const language = getWindowLanguage()
|
|
632
|
+
return (
|
|
633
|
+
fleetname[language] ||
|
|
634
|
+
fleetname['zh-CN'] ||
|
|
635
|
+
fleetname['zh-TW'] ||
|
|
636
|
+
fleetname.ja ||
|
|
637
|
+
fleetname['ja-JP'] ||
|
|
638
|
+
fleetname['en-US'] ||
|
|
639
|
+
Object.values(fleetname).find(Array.isArray) ||
|
|
640
|
+
[]
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function getWindowLanguage() {
|
|
645
|
+
if (typeof window !== 'undefined' && window.language) {
|
|
646
|
+
return window.language
|
|
647
|
+
}
|
|
648
|
+
return 'zh-CN'
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function emptyTag(area) {
|
|
652
|
+
return {
|
|
653
|
+
area,
|
|
654
|
+
mapName: '',
|
|
655
|
+
fleetName: '',
|
|
656
|
+
color: '',
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
module.exports = {
|
|
661
|
+
createPoiDataBridge,
|
|
662
|
+
DEFAULT_PORT,
|
|
663
|
+
DEFAULT_PORT_FILE,
|
|
664
|
+
DEFAULT_PLANNER_FILE,
|
|
665
|
+
extractEventData,
|
|
666
|
+
extractMasterData,
|
|
667
|
+
extractPlannerData,
|
|
668
|
+
extractNames,
|
|
669
|
+
}
|