From b9815400b3698857f75442fce66efece7dd91912 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sun, 28 Jan 2018 00:45:36 +0000 Subject: [PATCH 01/15] Add Alliance Chieftain and Recon Limpet Controller --- companion.py | 1 + coriolis-data | 2 +- coriolis.py | 7 +- modules.p | 5828 +++++++++++++++++++++++++------------------------ outfitting.py | 1 + ships.p | 128 +- 6 files changed, 3034 insertions(+), 2933 deletions(-) diff --git a/companion.py b/companion.py index 9221e482..858f9180 100644 --- a/companion.py +++ b/companion.py @@ -73,6 +73,7 @@ ship_map = { 'type7' : 'Type-7 Transporter', 'type9' : 'Type-9 Heavy', 'type9_military' : 'Type-10 Defender', + 'typex' : 'Alliance Chieftain', 'viper' : 'Viper MkIII', 'viper_mkiv' : 'Viper MkIV', 'vulture' : 'Vulture', diff --git a/coriolis-data b/coriolis-data index a3ae3e34..44d57f26 160000 --- a/coriolis-data +++ b/coriolis-data @@ -1 +1 @@ -Subproject commit a3ae3e34ad879d1504a7d2fdc3fce4854820051b +Subproject commit 44d57f26a8099cfa8d91815f453c7bba303a63ec diff --git a/coriolis.py b/coriolis.py index 4e947fb4..433aa390 100755 --- a/coriolis.py +++ b/coriolis.py @@ -169,11 +169,16 @@ if __name__ == "__main__": else: modules[key] = { 'mass': m.get('mass', 0) } # Some modules don't have mass - # 2.5 additions not yet present in coriolis-data + # 3.0 additions not yet present in coriolis-data modules[('Decontamination Limpet Controller', None, '1', 'E')] = {'mass': 1.3} modules[('Decontamination Limpet Controller', None, '3', 'E')] = {'mass': 2} modules[('Decontamination Limpet Controller', None, '5', 'E')] = {'mass': 20} modules[('Decontamination Limpet Controller', None, '7', 'E')] = {'mass': 128} + modules[('Recon Limpet Controller', None, '1', 'E')] = {'mass': 1.3} + modules[('Recon Limpet Controller', None, '3', 'E')] = {'mass': 2} + modules[('Recon Limpet Controller', None, '5', 'E')] = {'mass': 20} + modules[('Recon Limpet Controller', None, '7', 'E')] = {'mass': 128} + modules[('Research Limpet Controller', None, '1', 'E')] = {'mass': 1.3} modules = OrderedDict([(k,modules[k]) for k in sorted(modules)]) # sort for easier diffing cPickle.dump(modules, open('modules.p', 'wb')) diff --git a/modules.p b/modules.p index 2d1a1cb5..9e6e42ba 100644 --- a/modules.p +++ b/modules.p @@ -3139,7 +3139,7 @@ g7 I0 saa(lp1102 (g1098 -S'Anaconda' +S'Alliance Chieftain' p1103 S'1' S'I' @@ -3149,7 +3149,7 @@ g7 I0 saa(lp1106 (g1098 -S'Asp Explorer' +S'Anaconda' p1107 S'1' S'I' @@ -3159,7 +3159,7 @@ g7 I0 saa(lp1110 (g1098 -S'Asp Scout' +S'Asp Explorer' p1111 S'1' S'I' @@ -3169,7 +3169,7 @@ g7 I0 saa(lp1114 (g1098 -S'Beluga Liner' +S'Asp Scout' p1115 S'1' S'I' @@ -3179,7 +3179,7 @@ g7 I0 saa(lp1118 (g1098 -S'Cobra MkIII' +S'Beluga Liner' p1119 S'1' S'I' @@ -3189,7 +3189,7 @@ g7 I0 saa(lp1122 (g1098 -S'Cobra MkIV' +S'Cobra MkIII' p1123 S'1' S'I' @@ -3199,7 +3199,7 @@ g7 I0 saa(lp1126 (g1098 -S'Diamondback Explorer' +S'Cobra MkIV' p1127 S'1' S'I' @@ -3209,7 +3209,7 @@ g7 I0 saa(lp1130 (g1098 -S'Diamondback Scout' +S'Diamondback Explorer' p1131 S'1' S'I' @@ -3219,7 +3219,7 @@ g7 I0 saa(lp1134 (g1098 -S'Dolphin' +S'Diamondback Scout' p1135 S'1' S'I' @@ -3229,7 +3229,7 @@ g7 I0 saa(lp1138 (g1098 -S'Eagle' +S'Dolphin' p1139 S'1' S'I' @@ -3239,7 +3239,7 @@ g7 I0 saa(lp1142 (g1098 -S'Federal Assault Ship' +S'Eagle' p1143 S'1' S'I' @@ -3249,7 +3249,7 @@ g7 I0 saa(lp1146 (g1098 -S'Federal Corvette' +S'Federal Assault Ship' p1147 S'1' S'I' @@ -3259,7 +3259,7 @@ g7 I0 saa(lp1150 (g1098 -S'Federal Dropship' +S'Federal Corvette' p1151 S'1' S'I' @@ -3269,7 +3269,7 @@ g7 I0 saa(lp1154 (g1098 -S'Federal Gunship' +S'Federal Dropship' p1155 S'1' S'I' @@ -3279,7 +3279,7 @@ g7 I0 saa(lp1158 (g1098 -S'Fer-de-Lance' +S'Federal Gunship' p1159 S'1' S'I' @@ -3289,7 +3289,7 @@ g7 I0 saa(lp1162 (g1098 -S'Hauler' +S'Fer-de-Lance' p1163 S'1' S'I' @@ -3299,7 +3299,7 @@ g7 I0 saa(lp1166 (g1098 -S'Imperial Clipper' +S'Hauler' p1167 S'1' S'I' @@ -3309,7 +3309,7 @@ g7 I0 saa(lp1170 (g1098 -S'Imperial Courier' +S'Imperial Clipper' p1171 S'1' S'I' @@ -3319,7 +3319,7 @@ g7 I0 saa(lp1174 (g1098 -S'Imperial Cutter' +S'Imperial Courier' p1175 S'1' S'I' @@ -3329,7 +3329,7 @@ g7 I0 saa(lp1178 (g1098 -S'Imperial Eagle' +S'Imperial Cutter' p1179 S'1' S'I' @@ -3339,7 +3339,7 @@ g7 I0 saa(lp1182 (g1098 -S'Keelback' +S'Imperial Eagle' p1183 S'1' S'I' @@ -3349,7 +3349,7 @@ g7 I0 saa(lp1186 (g1098 -S'Orca' +S'Keelback' p1187 S'1' S'I' @@ -3359,7 +3359,7 @@ g7 I0 saa(lp1190 (g1098 -S'Python' +S'Orca' p1191 S'1' S'I' @@ -3369,7 +3369,7 @@ g7 I0 saa(lp1194 (g1098 -S'Sidewinder' +S'Python' p1195 S'1' S'I' @@ -3379,7 +3379,7 @@ g7 I0 saa(lp1198 (g1098 -S'Type-10 Defender' +S'Sidewinder' p1199 S'1' S'I' @@ -3389,7 +3389,7 @@ g7 I0 saa(lp1202 (g1098 -S'Type-6 Transporter' +S'Type-10 Defender' p1203 S'1' S'I' @@ -3399,7 +3399,7 @@ g7 I0 saa(lp1206 (g1098 -S'Type-7 Transporter' +S'Type-6 Transporter' p1207 S'1' S'I' @@ -3409,7 +3409,7 @@ g7 I0 saa(lp1210 (g1098 -S'Type-9 Heavy' +S'Type-7 Transporter' p1211 S'1' S'I' @@ -3419,7 +3419,7 @@ g7 I0 saa(lp1214 (g1098 -S'Viper MkIII' +S'Type-9 Heavy' p1215 S'1' S'I' @@ -3429,7 +3429,7 @@ g7 I0 saa(lp1218 (g1098 -S'Viper MkIV' +S'Viper MkIII' p1219 S'1' S'I' @@ -3439,7 +3439,7 @@ g7 I0 saa(lp1222 (g1098 -S'Vulture' +S'Viper MkIV' p1223 S'1' S'I' @@ -3448,1756 +3448,1759 @@ a(dp1225 g7 I0 saa(lp1226 -(S'Luxury Class Passenger Cabin' +(g1098 +S'Vulture' p1227 -NS'5' -S'B' +S'1' +S'I' tp1228 a(dp1229 g7 -I20 +I0 saa(lp1230 -(g1227 +(S'Luxury Class Passenger Cabin' +p1231 +NS'5' +S'B' +tp1232 +a(dp1233 +g7 +I20 +saa(lp1234 +(g1231 NS'6' S'B' -tp1231 -a(dp1232 -g7 -I40 -saa(lp1233 -(S'Military Grade Composite' -p1234 -g1099 -S'1' -S'I' tp1235 a(dp1236 g7 -I5 +I40 saa(lp1237 -(g1234 +(S'Military Grade Composite' +p1238 +g1099 +S'1' +S'I' +tp1239 +a(dp1240 +g7 +I5 +saa(lp1241 +(g1238 g1103 S'1' S'I' -tp1238 -a(dp1239 +tp1242 +a(dp1243 g7 -I60 -saa(lp1240 -(g1234 +I150 +saa(lp1244 +(g1238 g1107 S'1' S'I' -tp1241 -a(dp1242 +tp1245 +a(dp1246 g7 -I42 -saa(lp1243 -(g1234 +I60 +saa(lp1247 +(g1238 g1111 S'1' S'I' -tp1244 -a(dp1245 +tp1248 +a(dp1249 g7 I42 -saa(lp1246 -(g1234 +saa(lp1250 +(g1238 g1115 S'1' S'I' -tp1247 -a(dp1248 +tp1251 +a(dp1252 g7 -I165 -saa(lp1249 -(g1234 +I42 +saa(lp1253 +(g1238 g1119 S'1' S'I' -tp1250 -a(dp1251 +tp1254 +a(dp1255 g7 -I27 -saa(lp1252 -(g1234 +I165 +saa(lp1256 +(g1238 g1123 S'1' S'I' -tp1253 -a(dp1254 +tp1257 +a(dp1258 g7 I27 -saa(lp1255 -(g1234 +saa(lp1259 +(g1238 g1127 S'1' S'I' -tp1256 -a(dp1257 +tp1260 +a(dp1261 g7 -I47 -saa(lp1258 -(g1234 +I27 +saa(lp1262 +(g1238 g1131 S'1' S'I' -tp1259 -a(dp1260 +tp1263 +a(dp1264 g7 -I26 -saa(lp1261 -(g1234 +I47 +saa(lp1265 +(g1238 g1135 S'1' S'I' -tp1262 -a(dp1263 +tp1266 +a(dp1267 g7 -I63 -saa(lp1264 -(g1234 +I26 +saa(lp1268 +(g1238 g1139 S'1' S'I' -tp1265 -a(dp1266 +tp1269 +a(dp1270 g7 -I8 -saa(lp1267 -(g1234 +I63 +saa(lp1271 +(g1238 g1143 S'1' S'I' -tp1268 -a(dp1269 +tp1272 +a(dp1273 g7 -I87 -saa(lp1270 -(g1234 +I8 +saa(lp1274 +(g1238 g1147 S'1' S'I' -tp1271 -a(dp1272 +tp1275 +a(dp1276 g7 -I60 -saa(lp1273 -(g1234 +I87 +saa(lp1277 +(g1238 g1151 S'1' S'I' -tp1274 -a(dp1275 +tp1278 +a(dp1279 g7 -I87 -saa(lp1276 -(g1234 +I60 +saa(lp1280 +(g1238 g1155 S'1' S'I' -tp1277 -a(dp1278 +tp1281 +a(dp1282 g7 I87 -saa(lp1279 -(g1234 +saa(lp1283 +(g1238 g1159 S'1' S'I' -tp1280 -a(dp1281 +tp1284 +a(dp1285 g7 -I38 -saa(lp1282 -(g1234 +I87 +saa(lp1286 +(g1238 g1163 S'1' S'I' -tp1283 -a(dp1284 +tp1287 +a(dp1288 g7 -I2 -saa(lp1285 -(g1234 +I38 +saa(lp1289 +(g1238 g1167 S'1' S'I' -tp1286 -a(dp1287 +tp1290 +a(dp1291 g7 -I60 -saa(lp1288 -(g1234 +I2 +saa(lp1292 +(g1238 g1171 S'1' S'I' -tp1289 -a(dp1290 +tp1293 +a(dp1294 g7 -I8 -saa(lp1291 -(g1234 +I60 +saa(lp1295 +(g1238 g1175 S'1' S'I' -tp1292 -a(dp1293 +tp1296 +a(dp1297 g7 -I60 -saa(lp1294 -(g1234 +I8 +saa(lp1298 +(g1238 g1179 S'1' S'I' -tp1295 -a(dp1296 +tp1299 +a(dp1300 g7 -I8 -saa(lp1297 -(g1234 +I60 +saa(lp1301 +(g1238 g1183 S'1' S'I' -tp1298 -a(dp1299 +tp1302 +a(dp1303 g7 -I23 -saa(lp1300 -(g1234 +I8 +saa(lp1304 +(g1238 g1187 S'1' S'I' -tp1301 -a(dp1302 +tp1305 +a(dp1306 g7 -I87 -saa(lp1303 -(g1234 +I23 +saa(lp1307 +(g1238 g1191 S'1' S'I' -tp1304 -a(dp1305 +tp1308 +a(dp1309 g7 -I53 -saa(lp1306 -(g1234 +I87 +saa(lp1310 +(g1238 g1195 S'1' S'I' -tp1307 -a(dp1308 +tp1311 +a(dp1312 g7 -I4 -saa(lp1309 -(g1234 +I53 +saa(lp1313 +(g1238 g1199 S'1' S'I' -tp1310 -a(dp1311 +tp1314 +a(dp1315 g7 -I150 -saa(lp1312 -(g1234 +I4 +saa(lp1316 +(g1238 g1203 S'1' S'I' -tp1313 -a(dp1314 +tp1317 +a(dp1318 g7 -I23 -saa(lp1315 -(g1234 +I150 +saa(lp1319 +(g1238 g1207 S'1' S'I' -tp1316 -a(dp1317 +tp1320 +a(dp1321 g7 -I63 -saa(lp1318 -(g1234 +I23 +saa(lp1322 +(g1238 g1211 S'1' S'I' -tp1319 -a(dp1320 +tp1323 +a(dp1324 g7 -I150 -saa(lp1321 -(g1234 +I63 +saa(lp1325 +(g1238 g1215 S'1' S'I' -tp1322 -a(dp1323 +tp1326 +a(dp1327 g7 -I9 -saa(lp1324 -(g1234 +I150 +saa(lp1328 +(g1238 g1219 S'1' S'I' -tp1325 -a(dp1326 +tp1329 +a(dp1330 g7 I9 -saa(lp1327 -(g1234 +saa(lp1331 +(g1238 g1223 S'1' S'I' -tp1328 -a(dp1329 -g7 -I35 -saa(lp1330 -(S'Mine Launcher' -p1331 -NS'1' -S'I' tp1332 a(dp1333 g7 -I2 +I9 saa(lp1334 -(g1331 -NS'2' +(g1238 +g1227 +S'1' S'I' tp1335 a(dp1336 g7 -I4 +I35 saa(lp1337 -(S'Mining Lance Beam Laser' +(S'Mine Launcher' p1338 NS'1' -S'D' +S'I' tp1339 a(dp1340 g7 I2 saa(lp1341 -(S'Mining Laser' -p1342 -NS'1' -S'D' -tp1343 -a(dp1344 -g7 -I2 -saa(lp1345 -(g1342 +(g1338 NS'2' +S'I' +tp1342 +a(dp1343 +g7 +I4 +saa(lp1344 +(S'Mining Lance Beam Laser' +p1345 +NS'1' S'D' tp1346 a(dp1347 g7 I2 saa(lp1348 -(S'Mirrored Surface Composite' +(S'Mining Laser' p1349 -g1099 -S'1' -S'I' +NS'1' +S'D' tp1350 a(dp1351 g7 -I5 +I2 saa(lp1352 (g1349 -g1103 -S'1' -S'I' +NS'2' +S'D' tp1353 a(dp1354 g7 -I60 +I2 saa(lp1355 -(g1349 +(S'Mirrored Surface Composite' +p1356 +g1099 +S'1' +S'I' +tp1357 +a(dp1358 +g7 +I5 +saa(lp1359 +(g1356 +g1103 +S'1' +S'I' +tp1360 +a(dp1361 +g7 +I150 +saa(lp1362 +(g1356 g1107 S'1' S'I' -tp1356 -a(dp1357 +tp1363 +a(dp1364 g7 -I42 -saa(lp1358 -(g1349 +I60 +saa(lp1365 +(g1356 g1111 S'1' S'I' -tp1359 -a(dp1360 +tp1366 +a(dp1367 g7 I42 -saa(lp1361 -(g1349 +saa(lp1368 +(g1356 g1115 S'1' S'I' -tp1362 -a(dp1363 +tp1369 +a(dp1370 g7 -I165 -saa(lp1364 -(g1349 +I42 +saa(lp1371 +(g1356 g1119 S'1' S'I' -tp1365 -a(dp1366 +tp1372 +a(dp1373 g7 -I27 -saa(lp1367 -(g1349 +I165 +saa(lp1374 +(g1356 g1123 S'1' S'I' -tp1368 -a(dp1369 +tp1375 +a(dp1376 g7 I27 -saa(lp1370 -(g1349 +saa(lp1377 +(g1356 g1127 S'1' S'I' -tp1371 -a(dp1372 +tp1378 +a(dp1379 g7 -I26 -saa(lp1373 -(g1349 +I27 +saa(lp1380 +(g1356 g1131 S'1' S'I' -tp1374 -a(dp1375 +tp1381 +a(dp1382 g7 I26 -saa(lp1376 -(g1349 +saa(lp1383 +(g1356 g1135 S'1' S'I' -tp1377 -a(dp1378 +tp1384 +a(dp1385 g7 -I63 -saa(lp1379 -(g1349 +I26 +saa(lp1386 +(g1356 g1139 S'1' S'I' -tp1380 -a(dp1381 +tp1387 +a(dp1388 g7 -I8 -saa(lp1382 -(g1349 +I63 +saa(lp1389 +(g1356 g1143 S'1' S'I' -tp1383 -a(dp1384 +tp1390 +a(dp1391 g7 -I87 -saa(lp1385 -(g1349 +I8 +saa(lp1392 +(g1356 g1147 S'1' S'I' -tp1386 -a(dp1387 +tp1393 +a(dp1394 g7 -I60 -saa(lp1388 -(g1349 +I87 +saa(lp1395 +(g1356 g1151 S'1' S'I' -tp1389 -a(dp1390 +tp1396 +a(dp1397 g7 -I87 -saa(lp1391 -(g1349 +I60 +saa(lp1398 +(g1356 g1155 S'1' S'I' -tp1392 -a(dp1393 +tp1399 +a(dp1400 g7 I87 -saa(lp1394 -(g1349 +saa(lp1401 +(g1356 g1159 S'1' S'I' -tp1395 -a(dp1396 +tp1402 +a(dp1403 g7 -I38 -saa(lp1397 -(g1349 +I87 +saa(lp1404 +(g1356 g1163 S'1' S'I' -tp1398 -a(dp1399 +tp1405 +a(dp1406 g7 -I2 -saa(lp1400 -(g1349 +I38 +saa(lp1407 +(g1356 g1167 S'1' S'I' -tp1401 -a(dp1402 +tp1408 +a(dp1409 g7 -I60 -saa(lp1403 -(g1349 +I2 +saa(lp1410 +(g1356 g1171 S'1' S'I' -tp1404 -a(dp1405 +tp1411 +a(dp1412 g7 -I8 -saa(lp1406 -(g1349 +I60 +saa(lp1413 +(g1356 g1175 S'1' S'I' -tp1407 -a(dp1408 +tp1414 +a(dp1415 g7 -I60 -saa(lp1409 -(g1349 +I8 +saa(lp1416 +(g1356 g1179 S'1' S'I' -tp1410 -a(dp1411 +tp1417 +a(dp1418 g7 -I8 -saa(lp1412 -(g1349 +I60 +saa(lp1419 +(g1356 g1183 S'1' S'I' -tp1413 -a(dp1414 +tp1420 +a(dp1421 g7 -I23 -saa(lp1415 -(g1349 +I8 +saa(lp1422 +(g1356 g1187 S'1' S'I' -tp1416 -a(dp1417 +tp1423 +a(dp1424 g7 -I87 -saa(lp1418 -(g1349 +I23 +saa(lp1425 +(g1356 g1191 S'1' S'I' -tp1419 -a(dp1420 +tp1426 +a(dp1427 g7 -I53 -saa(lp1421 -(g1349 +I87 +saa(lp1428 +(g1356 g1195 S'1' S'I' -tp1422 -a(dp1423 +tp1429 +a(dp1430 g7 -I4 -saa(lp1424 -(g1349 +I53 +saa(lp1431 +(g1356 g1199 S'1' S'I' -tp1425 -a(dp1426 +tp1432 +a(dp1433 g7 -I150 -saa(lp1427 -(g1349 +I4 +saa(lp1434 +(g1356 g1203 S'1' S'I' -tp1428 -a(dp1429 +tp1435 +a(dp1436 g7 -I23 -saa(lp1430 -(g1349 +I150 +saa(lp1437 +(g1356 g1207 S'1' S'I' -tp1431 -a(dp1432 +tp1438 +a(dp1439 g7 -I63 -saa(lp1433 -(g1349 +I23 +saa(lp1440 +(g1356 g1211 S'1' S'I' -tp1434 -a(dp1435 +tp1441 +a(dp1442 g7 -I150 -saa(lp1436 -(g1349 +I63 +saa(lp1443 +(g1356 g1215 S'1' S'I' -tp1437 -a(dp1438 +tp1444 +a(dp1445 g7 -I9 -saa(lp1439 -(g1349 +I150 +saa(lp1446 +(g1356 g1219 S'1' S'I' -tp1440 -a(dp1441 -g7 -I9 -saa(lp1442 -(g1349 -g1223 -S'1' -S'I' -tp1443 -a(dp1444 -g7 -I35 -saa(lp1445 -(S'Missile Rack' -p1446 -NS'1' -S'B' tp1447 a(dp1448 g7 -I2 +I9 saa(lp1449 -(g1446 -NS'2' -S'B' +(g1356 +g1223 +S'1' +S'I' tp1450 a(dp1451 g7 -I4 +I9 saa(lp1452 -(S'Module Reinforcement Package' -p1453 -NS'1' -S'D' -tp1454 -a(dp1455 +(g1356 +g1227 +S'1' +S'I' +tp1453 +a(dp1454 g7 -I1 -saa(lp1456 -(g1453 +I35 +saa(lp1455 +(S'Missile Rack' +p1456 NS'1' -S'E' +S'B' tp1457 a(dp1458 g7 I2 saa(lp1459 -(g1453 +(g1456 NS'2' -S'D' +S'B' tp1460 a(dp1461 g7 -I2 +I4 saa(lp1462 -(g1453 +(S'Module Reinforcement Package' +p1463 +NS'1' +S'D' +tp1464 +a(dp1465 +g7 +I1 +saa(lp1466 +(g1463 +NS'1' +S'E' +tp1467 +a(dp1468 +g7 +I2 +saa(lp1469 +(g1463 +NS'2' +S'D' +tp1470 +a(dp1471 +g7 +I2 +saa(lp1472 +(g1463 NS'2' S'E' -tp1463 -a(dp1464 +tp1473 +a(dp1474 g7 I4 -saa(lp1465 -(g1453 +saa(lp1475 +(g1463 NS'3' S'D' -tp1466 -a(dp1467 +tp1476 +a(dp1477 g7 I4 -saa(lp1468 -(g1453 +saa(lp1478 +(g1463 NS'3' S'E' -tp1469 -a(dp1470 +tp1479 +a(dp1480 g7 I8 -saa(lp1471 -(g1453 +saa(lp1481 +(g1463 NS'4' S'D' -tp1472 -a(dp1473 +tp1482 +a(dp1483 g7 I8 -saa(lp1474 -(g1453 +saa(lp1484 +(g1463 NS'4' S'E' -tp1475 -a(dp1476 -g7 -I16 -saa(lp1477 -(g1453 -NS'5' -S'D' -tp1478 -a(dp1479 -g7 -I16 -saa(lp1480 -(g1453 -NS'5' -S'E' -tp1481 -a(dp1482 -g7 -I32 -saa(lp1483 -(S'Multi-Cannon' -p1484 -NS'1' -S'F' tp1485 a(dp1486 g7 -I2 +I16 saa(lp1487 -(g1484 -NS'1' -S'G' +(g1463 +NS'5' +S'D' tp1488 a(dp1489 g7 -I2 +I16 saa(lp1490 -(g1484 -NS'2' +(g1463 +NS'5' S'E' tp1491 a(dp1492 g7 -I4 +I32 saa(lp1493 -(g1484 +(S'Multi-Cannon' +p1494 +NS'1' +S'F' +tp1495 +a(dp1496 +g7 +I2 +saa(lp1497 +(g1494 +NS'1' +S'G' +tp1498 +a(dp1499 +g7 +I2 +saa(lp1500 +(g1494 +NS'2' +S'E' +tp1501 +a(dp1502 +g7 +I4 +saa(lp1503 +(g1494 NS'2' S'F' -tp1494 -a(dp1495 -g7 -I4 -saa(lp1496 -(g1484 -NS'3' -S'C' -tp1497 -a(dp1498 -g7 -I8 -saa(lp1499 -(g1484 -NS'4' -S'A' -tp1500 -a(dp1501 -g7 -I16 -saa(lp1502 -(S'Pacifier Frag-Cannon' -p1503 -NS'3' -S'C' tp1504 a(dp1505 g7 -I8 +I4 saa(lp1506 +(g1494 +NS'3' +S'C' +tp1507 +a(dp1508 +g7 +I8 +saa(lp1509 +(g1494 +NS'4' +S'A' +tp1510 +a(dp1511 +g7 +I16 +saa(lp1512 +(S'Pacifier Frag-Cannon' +p1513 +NS'3' +S'C' +tp1514 +a(dp1515 +g7 +I8 +saa(lp1516 (S'Pack-Hound Missile Rack' -p1507 +p1517 NS'2' S'B' -tp1508 -a(dp1509 +tp1518 +a(dp1519 g7 I4 -saa(lp1510 +saa(lp1520 (S'Planetary Approach Suite' -p1511 +p1521 NS'1' S'I' -tp1512 -a(dp1513 -g7 -I0 -saa(lp1514 -(S'Planetary Vehicle Hangar' -p1515 -NS'2' -S'G' -tp1516 -a(dp1517 -g7 -I6 -saa(lp1518 -(g1515 -NS'2' -S'H' -tp1519 -a(dp1520 -g7 -I12 -saa(lp1521 -(g1515 -NS'4' -S'G' tp1522 a(dp1523 g7 -I10 +I0 saa(lp1524 -(g1515 +(S'Planetary Vehicle Hangar' +p1525 +NS'2' +S'G' +tp1526 +a(dp1527 +g7 +I6 +saa(lp1528 +(g1525 +NS'2' +S'H' +tp1529 +a(dp1530 +g7 +I12 +saa(lp1531 +(g1525 +NS'4' +S'G' +tp1532 +a(dp1533 +g7 +I10 +saa(lp1534 +(g1525 NS'4' S'H' -tp1525 -a(dp1526 -g7 -I20 -saa(lp1527 -(g1515 -NS'6' -S'G' -tp1528 -a(dp1529 -g7 -I17 -saa(lp1530 -(g1515 -NS'6' -S'H' -tp1531 -a(dp1532 -g7 -I34 -saa(lp1533 -(S'Plasma Accelerator' -p1534 -NS'2' -S'C' tp1535 a(dp1536 g7 -I4 +I20 saa(lp1537 -(g1534 -NS'3' -S'B' +(g1525 +NS'6' +S'G' tp1538 a(dp1539 g7 -I8 +I17 saa(lp1540 -(g1534 -NS'4' -S'A' +(g1525 +NS'6' +S'H' tp1541 a(dp1542 g7 -I16 +I34 saa(lp1543 -(S'Point Defence' +(S'Plasma Accelerator' p1544 -NS'0' -S'I' +NS'2' +S'C' tp1545 a(dp1546 g7 -F0.5 +I4 saa(lp1547 -(S'Power Distributor' -p1548 -NS'1' -S'A' -tp1549 -a(dp1550 -g7 -F1.3 -saa(lp1551 -(g1548 -NS'1' +(g1544 +NS'3' S'B' -tp1552 -a(dp1553 +tp1548 +a(dp1549 g7 -I2 -saa(lp1554 -(g1548 -NS'1' -S'C' +I8 +saa(lp1550 +(g1544 +NS'4' +S'A' +tp1551 +a(dp1552 +g7 +I16 +saa(lp1553 +(S'Point Defence' +p1554 +NS'0' +S'I' tp1555 a(dp1556 g7 -F1.3 -saa(lp1557 -(g1548 -NS'1' -S'D' -tp1558 -a(dp1559 -g7 F0.5 -saa(lp1560 -(g1548 +saa(lp1557 +(S'Power Distributor' +p1558 NS'1' -S'E' -tp1561 -a(dp1562 +S'A' +tp1559 +a(dp1560 g7 F1.3 -saa(lp1563 -(g1548 -NS'2' -S'A' -tp1564 -a(dp1565 -g7 -F2.5 -saa(lp1566 -(g1548 -NS'2' +saa(lp1561 +(g1558 +NS'1' S'B' -tp1567 -a(dp1568 -g7 -I4 -saa(lp1569 -(g1548 -NS'2' -S'C' -tp1570 -a(dp1571 -g7 -F2.5 -saa(lp1572 -(g1548 -NS'2' -S'D' -tp1573 -a(dp1574 -g7 -I1 -saa(lp1575 -(g1548 -NS'2' -S'E' -tp1576 -a(dp1577 -g7 -F2.5 -saa(lp1578 -(g1548 -NS'3' -S'A' -tp1579 -a(dp1580 -g7 -I5 -saa(lp1581 -(g1548 -NS'3' -S'B' -tp1582 -a(dp1583 -g7 -I8 -saa(lp1584 -(g1548 -NS'3' -S'C' -tp1585 -a(dp1586 -g7 -I5 -saa(lp1587 -(g1548 -NS'3' -S'D' -tp1588 -a(dp1589 +tp1562 +a(dp1563 g7 I2 -saa(lp1590 -(g1548 -NS'3' -S'E' -tp1591 -a(dp1592 -g7 -I5 -saa(lp1593 -(g1548 -NS'4' -S'A' -tp1594 -a(dp1595 -g7 -I10 -saa(lp1596 -(g1548 -NS'4' -S'B' -tp1597 -a(dp1598 -g7 -I16 -saa(lp1599 -(g1548 -NS'4' +saa(lp1564 +(g1558 +NS'1' S'C' -tp1600 -a(dp1601 +tp1565 +a(dp1566 g7 -I10 -saa(lp1602 -(g1548 -NS'4' +F1.3 +saa(lp1567 +(g1558 +NS'1' S'D' -tp1603 -a(dp1604 +tp1568 +a(dp1569 g7 -I4 -saa(lp1605 -(g1548 -NS'4' +F0.5 +saa(lp1570 +(g1558 +NS'1' S'E' -tp1606 -a(dp1607 +tp1571 +a(dp1572 g7 -I10 -saa(lp1608 -(g1548 -NS'5' -S'A' -tp1609 -a(dp1610 -g7 -I20 -saa(lp1611 -(g1548 -NS'5' -S'B' -tp1612 -a(dp1613 -g7 -I32 -saa(lp1614 -(g1548 -NS'5' -S'C' -tp1615 -a(dp1616 -g7 -I20 -saa(lp1617 -(g1548 -NS'5' -S'D' -tp1618 -a(dp1619 -g7 -I8 -saa(lp1620 -(g1548 -NS'5' -S'E' -tp1621 -a(dp1622 -g7 -I20 -saa(lp1623 -(g1548 -NS'6' -S'A' -tp1624 -a(dp1625 -g7 -I40 -saa(lp1626 -(g1548 -NS'6' -S'B' -tp1627 -a(dp1628 -g7 -I64 -saa(lp1629 -(g1548 -NS'6' -S'C' -tp1630 -a(dp1631 -g7 -I40 -saa(lp1632 -(g1548 -NS'6' -S'D' -tp1633 -a(dp1634 -g7 -I16 -saa(lp1635 -(g1548 -NS'6' -S'E' -tp1636 -a(dp1637 -g7 -I40 -saa(lp1638 -(g1548 -NS'7' -S'A' -tp1639 -a(dp1640 -g7 -I80 -saa(lp1641 -(g1548 -NS'7' -S'B' -tp1642 -a(dp1643 -g7 -I128 -saa(lp1644 -(g1548 -NS'7' -S'C' -tp1645 -a(dp1646 -g7 -I80 -saa(lp1647 -(g1548 -NS'7' -S'D' -tp1648 -a(dp1649 -g7 -I32 -saa(lp1650 -(g1548 -NS'7' -S'E' -tp1651 -a(dp1652 -g7 -I80 -saa(lp1653 -(g1548 -NS'8' -S'A' -tp1654 -a(dp1655 -g7 -I160 -saa(lp1656 -(g1548 -NS'8' -S'B' -tp1657 -a(dp1658 -g7 -I256 -saa(lp1659 -(g1548 -NS'8' -S'C' -tp1660 -a(dp1661 -g7 -I160 -saa(lp1662 -(g1548 -NS'8' -S'D' -tp1663 -a(dp1664 -g7 -I64 -saa(lp1665 -(g1548 -NS'8' -S'E' -tp1666 -a(dp1667 -g7 -I160 -saa(lp1668 -(S'Power Plant' -p1669 +F1.3 +saa(lp1573 +(g1558 NS'2' S'A' +tp1574 +a(dp1575 +g7 +F2.5 +saa(lp1576 +(g1558 +NS'2' +S'B' +tp1577 +a(dp1578 +g7 +I4 +saa(lp1579 +(g1558 +NS'2' +S'C' +tp1580 +a(dp1581 +g7 +F2.5 +saa(lp1582 +(g1558 +NS'2' +S'D' +tp1583 +a(dp1584 +g7 +I1 +saa(lp1585 +(g1558 +NS'2' +S'E' +tp1586 +a(dp1587 +g7 +F2.5 +saa(lp1588 +(g1558 +NS'3' +S'A' +tp1589 +a(dp1590 +g7 +I5 +saa(lp1591 +(g1558 +NS'3' +S'B' +tp1592 +a(dp1593 +g7 +I8 +saa(lp1594 +(g1558 +NS'3' +S'C' +tp1595 +a(dp1596 +g7 +I5 +saa(lp1597 +(g1558 +NS'3' +S'D' +tp1598 +a(dp1599 +g7 +I2 +saa(lp1600 +(g1558 +NS'3' +S'E' +tp1601 +a(dp1602 +g7 +I5 +saa(lp1603 +(g1558 +NS'4' +S'A' +tp1604 +a(dp1605 +g7 +I10 +saa(lp1606 +(g1558 +NS'4' +S'B' +tp1607 +a(dp1608 +g7 +I16 +saa(lp1609 +(g1558 +NS'4' +S'C' +tp1610 +a(dp1611 +g7 +I10 +saa(lp1612 +(g1558 +NS'4' +S'D' +tp1613 +a(dp1614 +g7 +I4 +saa(lp1615 +(g1558 +NS'4' +S'E' +tp1616 +a(dp1617 +g7 +I10 +saa(lp1618 +(g1558 +NS'5' +S'A' +tp1619 +a(dp1620 +g7 +I20 +saa(lp1621 +(g1558 +NS'5' +S'B' +tp1622 +a(dp1623 +g7 +I32 +saa(lp1624 +(g1558 +NS'5' +S'C' +tp1625 +a(dp1626 +g7 +I20 +saa(lp1627 +(g1558 +NS'5' +S'D' +tp1628 +a(dp1629 +g7 +I8 +saa(lp1630 +(g1558 +NS'5' +S'E' +tp1631 +a(dp1632 +g7 +I20 +saa(lp1633 +(g1558 +NS'6' +S'A' +tp1634 +a(dp1635 +g7 +I40 +saa(lp1636 +(g1558 +NS'6' +S'B' +tp1637 +a(dp1638 +g7 +I64 +saa(lp1639 +(g1558 +NS'6' +S'C' +tp1640 +a(dp1641 +g7 +I40 +saa(lp1642 +(g1558 +NS'6' +S'D' +tp1643 +a(dp1644 +g7 +I16 +saa(lp1645 +(g1558 +NS'6' +S'E' +tp1646 +a(dp1647 +g7 +I40 +saa(lp1648 +(g1558 +NS'7' +S'A' +tp1649 +a(dp1650 +g7 +I80 +saa(lp1651 +(g1558 +NS'7' +S'B' +tp1652 +a(dp1653 +g7 +I128 +saa(lp1654 +(g1558 +NS'7' +S'C' +tp1655 +a(dp1656 +g7 +I80 +saa(lp1657 +(g1558 +NS'7' +S'D' +tp1658 +a(dp1659 +g7 +I32 +saa(lp1660 +(g1558 +NS'7' +S'E' +tp1661 +a(dp1662 +g7 +I80 +saa(lp1663 +(g1558 +NS'8' +S'A' +tp1664 +a(dp1665 +g7 +I160 +saa(lp1666 +(g1558 +NS'8' +S'B' +tp1667 +a(dp1668 +g7 +I256 +saa(lp1669 +(g1558 +NS'8' +S'C' tp1670 a(dp1671 g7 -F1.3 +I160 saa(lp1672 -(g1669 -NS'2' -S'B' +(g1558 +NS'8' +S'D' tp1673 a(dp1674 g7 -I2 +I64 saa(lp1675 -(g1669 -NS'2' -S'C' +(g1558 +NS'8' +S'E' tp1676 a(dp1677 g7 -F1.3 +I160 saa(lp1678 -(g1669 +(S'Power Plant' +p1679 NS'2' -S'D' -tp1679 -a(dp1680 -g7 -I1 -saa(lp1681 -(g1669 -NS'2' -S'E' -tp1682 -a(dp1683 -g7 -F2.5 -saa(lp1684 -(g1669 -NS'3' S'A' -tp1685 -a(dp1686 +tp1680 +a(dp1681 g7 -F2.5 -saa(lp1687 -(g1669 -NS'3' +F1.3 +saa(lp1682 +(g1679 +NS'2' S'B' -tp1688 -a(dp1689 -g7 -I4 -saa(lp1690 -(g1669 -NS'3' -S'C' -tp1691 -a(dp1692 -g7 -F2.5 -saa(lp1693 -(g1669 -NS'3' -S'D' -tp1694 -a(dp1695 +tp1683 +a(dp1684 g7 I2 -saa(lp1696 -(g1669 -NS'3' -S'E' -tp1697 -a(dp1698 -g7 -I5 -saa(lp1699 -(g1669 -NS'4' -S'A' -tp1700 -a(dp1701 -g7 -I5 -saa(lp1702 -(g1669 -NS'4' -S'B' -tp1703 -a(dp1704 -g7 -I8 -saa(lp1705 -(g1669 -NS'4' +saa(lp1685 +(g1679 +NS'2' S'C' -tp1706 -a(dp1707 +tp1686 +a(dp1687 g7 -I5 -saa(lp1708 -(g1669 -NS'4' +F1.3 +saa(lp1688 +(g1679 +NS'2' S'D' -tp1709 -a(dp1710 +tp1689 +a(dp1690 +g7 +I1 +saa(lp1691 +(g1679 +NS'2' +S'E' +tp1692 +a(dp1693 +g7 +F2.5 +saa(lp1694 +(g1679 +NS'3' +S'A' +tp1695 +a(dp1696 +g7 +F2.5 +saa(lp1697 +(g1679 +NS'3' +S'B' +tp1698 +a(dp1699 g7 I4 -saa(lp1711 -(g1669 -NS'4' -S'E' -tp1712 -a(dp1713 -g7 -I10 -saa(lp1714 -(g1669 -NS'5' -S'A' -tp1715 -a(dp1716 -g7 -I10 -saa(lp1717 -(g1669 -NS'5' -S'B' -tp1718 -a(dp1719 -g7 -I16 -saa(lp1720 -(g1669 -NS'5' +saa(lp1700 +(g1679 +NS'3' S'C' -tp1721 -a(dp1722 +tp1701 +a(dp1702 g7 -I10 -saa(lp1723 -(g1669 -NS'5' +F2.5 +saa(lp1703 +(g1679 +NS'3' S'D' -tp1724 -a(dp1725 +tp1704 +a(dp1705 +g7 +I2 +saa(lp1706 +(g1679 +NS'3' +S'E' +tp1707 +a(dp1708 +g7 +I5 +saa(lp1709 +(g1679 +NS'4' +S'A' +tp1710 +a(dp1711 +g7 +I5 +saa(lp1712 +(g1679 +NS'4' +S'B' +tp1713 +a(dp1714 g7 I8 -saa(lp1726 -(g1669 -NS'5' -S'E' -tp1727 -a(dp1728 -g7 -I20 -saa(lp1729 -(g1669 -NS'6' -S'A' -tp1730 -a(dp1731 -g7 -I20 -saa(lp1732 -(g1669 -NS'6' -S'B' -tp1733 -a(dp1734 -g7 -I32 -saa(lp1735 -(g1669 -NS'6' +saa(lp1715 +(g1679 +NS'4' S'C' -tp1736 -a(dp1737 +tp1716 +a(dp1717 g7 -I20 -saa(lp1738 -(g1669 -NS'6' +I5 +saa(lp1718 +(g1679 +NS'4' S'D' -tp1739 -a(dp1740 +tp1719 +a(dp1720 +g7 +I4 +saa(lp1721 +(g1679 +NS'4' +S'E' +tp1722 +a(dp1723 +g7 +I10 +saa(lp1724 +(g1679 +NS'5' +S'A' +tp1725 +a(dp1726 +g7 +I10 +saa(lp1727 +(g1679 +NS'5' +S'B' +tp1728 +a(dp1729 g7 I16 -saa(lp1741 -(g1669 -NS'6' -S'E' -tp1742 -a(dp1743 -g7 -I40 -saa(lp1744 -(g1669 -NS'7' -S'A' -tp1745 -a(dp1746 -g7 -I40 -saa(lp1747 -(g1669 -NS'7' -S'B' -tp1748 -a(dp1749 -g7 -I64 -saa(lp1750 -(g1669 -NS'7' +saa(lp1730 +(g1679 +NS'5' S'C' -tp1751 -a(dp1752 +tp1731 +a(dp1732 g7 -I40 -saa(lp1753 -(g1669 -NS'7' +I10 +saa(lp1733 +(g1679 +NS'5' S'D' -tp1754 -a(dp1755 +tp1734 +a(dp1735 +g7 +I8 +saa(lp1736 +(g1679 +NS'5' +S'E' +tp1737 +a(dp1738 +g7 +I20 +saa(lp1739 +(g1679 +NS'6' +S'A' +tp1740 +a(dp1741 +g7 +I20 +saa(lp1742 +(g1679 +NS'6' +S'B' +tp1743 +a(dp1744 g7 I32 -saa(lp1756 -(g1669 -NS'7' -S'E' -tp1757 -a(dp1758 -g7 -I80 -saa(lp1759 -(g1669 -NS'8' -S'A' -tp1760 -a(dp1761 -g7 -I80 -saa(lp1762 -(g1669 -NS'8' -S'B' -tp1763 -a(dp1764 -g7 -I128 -saa(lp1765 -(g1669 -NS'8' +saa(lp1745 +(g1679 +NS'6' S'C' -tp1766 -a(dp1767 +tp1746 +a(dp1747 g7 -I80 -saa(lp1768 -(g1669 -NS'8' +I20 +saa(lp1748 +(g1679 +NS'6' S'D' -tp1769 -a(dp1770 +tp1749 +a(dp1750 +g7 +I16 +saa(lp1751 +(g1679 +NS'6' +S'E' +tp1752 +a(dp1753 +g7 +I40 +saa(lp1754 +(g1679 +NS'7' +S'A' +tp1755 +a(dp1756 +g7 +I40 +saa(lp1757 +(g1679 +NS'7' +S'B' +tp1758 +a(dp1759 g7 I64 -saa(lp1771 -(g1669 -NS'8' -S'E' -tp1772 -a(dp1773 +saa(lp1760 +(g1679 +NS'7' +S'C' +tp1761 +a(dp1762 g7 -I160 -saa(lp1774 -(S'Prismatic Shield Generator' -p1775 -NS'1' +I40 +saa(lp1763 +(g1679 +NS'7' +S'D' +tp1764 +a(dp1765 +g7 +I32 +saa(lp1766 +(g1679 +NS'7' +S'E' +tp1767 +a(dp1768 +g7 +I80 +saa(lp1769 +(g1679 +NS'8' S'A' +tp1770 +a(dp1771 +g7 +I80 +saa(lp1772 +(g1679 +NS'8' +S'B' +tp1773 +a(dp1774 +g7 +I128 +saa(lp1775 +(g1679 +NS'8' +S'C' tp1776 a(dp1777 g7 -F2.5 +I80 saa(lp1778 -(g1775 -NS'2' -S'A' +(g1679 +NS'8' +S'D' tp1779 a(dp1780 g7 -I5 +I64 saa(lp1781 -(g1775 -NS'3' -S'A' +(g1679 +NS'8' +S'E' tp1782 a(dp1783 g7 -I10 +I160 saa(lp1784 -(g1775 +(S'Prismatic Shield Generator' +p1785 +NS'1' +S'A' +tp1786 +a(dp1787 +g7 +F2.5 +saa(lp1788 +(g1785 +NS'2' +S'A' +tp1789 +a(dp1790 +g7 +I5 +saa(lp1791 +(g1785 +NS'3' +S'A' +tp1792 +a(dp1793 +g7 +I10 +saa(lp1794 +(g1785 NS'4' S'A' -tp1785 -a(dp1786 +tp1795 +a(dp1796 g7 I20 -saa(lp1787 -(g1775 +saa(lp1797 +(g1785 NS'5' S'A' -tp1788 -a(dp1789 +tp1798 +a(dp1799 g7 I40 -saa(lp1790 -(g1775 +saa(lp1800 +(g1785 NS'6' S'A' -tp1791 -a(dp1792 -g7 -I80 -saa(lp1793 -(g1775 -NS'7' -S'A' -tp1794 -a(dp1795 -g7 -I160 -saa(lp1796 -(g1775 -NS'8' -S'A' -tp1797 -a(dp1798 -g7 -I320 -saa(lp1799 -(S'Prospector Limpet Controller' -p1800 -NS'1' -S'A' tp1801 a(dp1802 g7 -F1.3 +I80 saa(lp1803 -(g1800 -NS'1' -S'B' +(g1785 +NS'7' +S'A' tp1804 a(dp1805 g7 -I2 +I160 saa(lp1806 -(g1800 -NS'1' -S'C' +(g1785 +NS'8' +S'A' tp1807 a(dp1808 g7 -F1.3 +I320 saa(lp1809 -(g1800 +(S'Prospector Limpet Controller' +p1810 NS'1' -S'D' -tp1810 -a(dp1811 -g7 -F0.5 -saa(lp1812 -(g1800 -NS'1' -S'E' -tp1813 -a(dp1814 +S'A' +tp1811 +a(dp1812 g7 F1.3 -saa(lp1815 -(g1800 -NS'3' -S'A' -tp1816 -a(dp1817 -g7 -I5 -saa(lp1818 -(g1800 -NS'3' +saa(lp1813 +(g1810 +NS'1' S'B' -tp1819 -a(dp1820 -g7 -I8 -saa(lp1821 -(g1800 -NS'3' -S'C' -tp1822 -a(dp1823 -g7 -I5 -saa(lp1824 -(g1800 -NS'3' -S'D' -tp1825 -a(dp1826 +tp1814 +a(dp1815 g7 I2 -saa(lp1827 -(g1800 -NS'3' +saa(lp1816 +(g1810 +NS'1' +S'C' +tp1817 +a(dp1818 +g7 +F1.3 +saa(lp1819 +(g1810 +NS'1' +S'D' +tp1820 +a(dp1821 +g7 +F0.5 +saa(lp1822 +(g1810 +NS'1' S'E' -tp1828 -a(dp1829 +tp1823 +a(dp1824 +g7 +F1.3 +saa(lp1825 +(g1810 +NS'3' +S'A' +tp1826 +a(dp1827 g7 I5 -saa(lp1830 -(g1800 -NS'5' -S'A' -tp1831 -a(dp1832 -g7 -I20 -saa(lp1833 -(g1800 -NS'5' +saa(lp1828 +(g1810 +NS'3' S'B' -tp1834 -a(dp1835 -g7 -I32 -saa(lp1836 -(g1800 -NS'5' -S'C' -tp1837 -a(dp1838 -g7 -I20 -saa(lp1839 -(g1800 -NS'5' -S'D' -tp1840 -a(dp1841 +tp1829 +a(dp1830 g7 I8 -saa(lp1842 -(g1800 -NS'5' +saa(lp1831 +(g1810 +NS'3' +S'C' +tp1832 +a(dp1833 +g7 +I5 +saa(lp1834 +(g1810 +NS'3' +S'D' +tp1835 +a(dp1836 +g7 +I2 +saa(lp1837 +(g1810 +NS'3' S'E' -tp1843 -a(dp1844 +tp1838 +a(dp1839 +g7 +I5 +saa(lp1840 +(g1810 +NS'5' +S'A' +tp1841 +a(dp1842 g7 I20 -saa(lp1845 -(g1800 -NS'7' -S'A' -tp1846 -a(dp1847 -g7 -I80 -saa(lp1848 -(g1800 -NS'7' +saa(lp1843 +(g1810 +NS'5' S'B' -tp1849 -a(dp1850 -g7 -I128 -saa(lp1851 -(g1800 -NS'7' -S'C' -tp1852 -a(dp1853 -g7 -I80 -saa(lp1854 -(g1800 -NS'7' -S'D' -tp1855 -a(dp1856 +tp1844 +a(dp1845 g7 I32 -saa(lp1857 -(g1800 -NS'7' +saa(lp1846 +(g1810 +NS'5' +S'C' +tp1847 +a(dp1848 +g7 +I20 +saa(lp1849 +(g1810 +NS'5' +S'D' +tp1850 +a(dp1851 +g7 +I8 +saa(lp1852 +(g1810 +NS'5' S'E' -tp1858 -a(dp1859 +tp1853 +a(dp1854 +g7 +I20 +saa(lp1855 +(g1810 +NS'7' +S'A' +tp1856 +a(dp1857 g7 I80 -saa(lp1860 -(S'Pulse Disruptor Laser' -p1861 -NS'2' -S'E' +saa(lp1858 +(g1810 +NS'7' +S'B' +tp1859 +a(dp1860 +g7 +I128 +saa(lp1861 +(g1810 +NS'7' +S'C' tp1862 a(dp1863 g7 -I4 +I80 saa(lp1864 -(S'Pulse Laser' -p1865 -NS'1' -S'F' -tp1866 -a(dp1867 +(g1810 +NS'7' +S'D' +tp1865 +a(dp1866 g7 -I2 -saa(lp1868 -(g1865 -NS'1' -S'G' -tp1869 -a(dp1870 +I32 +saa(lp1867 +(g1810 +NS'7' +S'E' +tp1868 +a(dp1869 g7 -I2 -saa(lp1871 -(g1865 +I80 +saa(lp1870 +(S'Pulse Disruptor Laser' +p1871 NS'2' S'E' tp1872 @@ -5205,2294 +5208,2379 @@ a(dp1873 g7 I4 saa(lp1874 -(g1865 -NS'2' +(S'Pulse Laser' +p1875 +NS'1' S'F' -tp1875 -a(dp1876 +tp1876 +a(dp1877 +g7 +I2 +saa(lp1878 +(g1875 +NS'1' +S'G' +tp1879 +a(dp1880 +g7 +I2 +saa(lp1881 +(g1875 +NS'2' +S'E' +tp1882 +a(dp1883 g7 I4 -saa(lp1877 -(g1865 +saa(lp1884 +(g1875 +NS'2' +S'F' +tp1885 +a(dp1886 +g7 +I4 +saa(lp1887 +(g1875 NS'3' S'D' -tp1878 -a(dp1879 +tp1888 +a(dp1889 g7 I8 -saa(lp1880 -(g1865 +saa(lp1890 +(g1875 NS'3' S'E' -tp1881 -a(dp1882 -g7 -I8 -saa(lp1883 -(g1865 -NS'3' -S'F' -tp1884 -a(dp1885 -g7 -I8 -saa(lp1886 -(g1865 -NS'4' -S'A' -tp1887 -a(dp1888 -g7 -I16 -saa(lp1889 -(S'Rail Gun' -p1890 -NS'1' -S'D' tp1891 a(dp1892 g7 -I2 +I8 saa(lp1893 -(g1890 -NS'2' -S'B' +(g1875 +NS'3' +S'F' tp1894 a(dp1895 g7 -I4 +I8 saa(lp1896 -(S'Reactive Surface Composite' -p1897 -g1099 -S'1' -S'I' -tp1898 -a(dp1899 +(g1875 +NS'4' +S'A' +tp1897 +a(dp1898 g7 -I5 -saa(lp1900 -(g1897 -g1103 -S'1' -S'I' +I16 +saa(lp1899 +(S'Rail Gun' +p1900 +NS'1' +S'D' tp1901 a(dp1902 g7 -I60 +I2 saa(lp1903 -(g1897 -g1107 -S'1' -S'I' +(g1900 +NS'2' +S'B' tp1904 a(dp1905 g7 -I42 -saa(lp1906 -(g1897 -g1111 -S'1' -S'I' -tp1907 -a(dp1908 -g7 -I42 -saa(lp1909 -(g1897 -g1115 -S'1' -S'I' -tp1910 -a(dp1911 -g7 -I165 -saa(lp1912 -(g1897 -g1119 -S'1' -S'I' -tp1913 -a(dp1914 -g7 -I27 -saa(lp1915 -(g1897 -g1123 -S'1' -S'I' -tp1916 -a(dp1917 -g7 -I27 -saa(lp1918 -(g1897 -g1127 -S'1' -S'I' -tp1919 -a(dp1920 -g7 -I47 -saa(lp1921 -(g1897 -g1131 -S'1' -S'I' -tp1922 -a(dp1923 -g7 -I26 -saa(lp1924 -(g1897 -g1135 -S'1' -S'I' -tp1925 -a(dp1926 -g7 -I63 -saa(lp1927 -(g1897 -g1139 -S'1' -S'I' -tp1928 -a(dp1929 -g7 -I8 -saa(lp1930 -(g1897 -g1143 -S'1' -S'I' -tp1931 -a(dp1932 -g7 -I87 -saa(lp1933 -(g1897 -g1147 -S'1' -S'I' -tp1934 -a(dp1935 -g7 -I60 -saa(lp1936 -(g1897 -g1151 -S'1' -S'I' -tp1937 -a(dp1938 -g7 -I87 -saa(lp1939 -(g1897 -g1155 -S'1' -S'I' -tp1940 -a(dp1941 -g7 -I87 -saa(lp1942 -(g1897 -g1159 -S'1' -S'I' -tp1943 -a(dp1944 -g7 -I38 -saa(lp1945 -(g1897 -g1163 -S'1' -S'I' -tp1946 -a(dp1947 -g7 -I2 -saa(lp1948 -(g1897 -g1167 -S'1' -S'I' -tp1949 -a(dp1950 -g7 -I60 -saa(lp1951 -(g1897 -g1171 -S'1' -S'I' -tp1952 -a(dp1953 -g7 -I8 -saa(lp1954 -(g1897 -g1175 -S'1' -S'I' -tp1955 -a(dp1956 -g7 -I60 -saa(lp1957 -(g1897 -g1179 -S'1' -S'I' -tp1958 -a(dp1959 -g7 -I8 -saa(lp1960 -(g1897 -g1183 -S'1' -S'I' -tp1961 -a(dp1962 -g7 -I23 -saa(lp1963 -(g1897 -g1187 -S'1' -S'I' -tp1964 -a(dp1965 -g7 -I87 -saa(lp1966 -(g1897 -g1191 -S'1' -S'I' -tp1967 -a(dp1968 -g7 -I53 -saa(lp1969 -(g1897 -g1195 -S'1' -S'I' -tp1970 -a(dp1971 -g7 I4 -saa(lp1972 -(g1897 -g1199 -S'1' -S'I' -tp1973 -a(dp1974 -g7 -I150 -saa(lp1975 -(g1897 -g1203 -S'1' -S'I' -tp1976 -a(dp1977 -g7 -I23 -saa(lp1978 -(g1897 -g1207 -S'1' -S'I' -tp1979 -a(dp1980 -g7 -I63 -saa(lp1981 -(g1897 -g1211 -S'1' -S'I' -tp1982 -a(dp1983 -g7 -I150 -saa(lp1984 -(g1897 -g1215 -S'1' -S'I' -tp1985 -a(dp1986 -g7 -I9 -saa(lp1987 -(g1897 -g1219 -S'1' -S'I' -tp1988 -a(dp1989 -g7 -I9 -saa(lp1990 -(g1897 -g1223 -S'1' -S'I' -tp1991 -a(dp1992 -g7 -I35 -saa(lp1993 -(S'Refinery' -p1994 -NS'1' -S'A' -tp1995 -a(dp1996 -g7 -I0 -saa(lp1997 -(g1994 -NS'1' -S'B' -tp1998 -a(dp1999 -g7 -I0 -saa(lp2000 -(g1994 -NS'1' -S'C' -tp2001 -a(dp2002 -g7 -I0 -saa(lp2003 -(g1994 -NS'1' -S'D' -tp2004 -a(dp2005 -g7 -I0 -saa(lp2006 -(g1994 -NS'1' -S'E' -tp2007 -a(dp2008 -g7 -I0 -saa(lp2009 -(g1994 -NS'2' -S'A' -tp2010 -a(dp2011 -g7 -I0 -saa(lp2012 -(g1994 -NS'2' -S'B' -tp2013 -a(dp2014 -g7 -I0 -saa(lp2015 -(g1994 -NS'2' -S'C' -tp2016 -a(dp2017 -g7 -I0 -saa(lp2018 -(g1994 -NS'2' -S'D' -tp2019 -a(dp2020 -g7 -I0 -saa(lp2021 -(g1994 -NS'2' -S'E' -tp2022 -a(dp2023 -g7 -I0 -saa(lp2024 -(g1994 -NS'3' -S'A' -tp2025 -a(dp2026 -g7 -I0 -saa(lp2027 -(g1994 -NS'3' -S'B' -tp2028 -a(dp2029 -g7 -I0 -saa(lp2030 -(g1994 -NS'3' -S'C' -tp2031 -a(dp2032 -g7 -I0 -saa(lp2033 -(g1994 -NS'3' -S'D' -tp2034 -a(dp2035 -g7 -I0 -saa(lp2036 -(g1994 -NS'3' -S'E' -tp2037 -a(dp2038 -g7 -I0 -saa(lp2039 -(g1994 -NS'4' -S'A' -tp2040 -a(dp2041 -g7 -I0 -saa(lp2042 -(g1994 -NS'4' -S'B' -tp2043 -a(dp2044 -g7 -I0 -saa(lp2045 -(g1994 -NS'4' -S'C' -tp2046 -a(dp2047 -g7 -I0 -saa(lp2048 -(g1994 -NS'4' -S'D' -tp2049 -a(dp2050 -g7 -I0 -saa(lp2051 -(g1994 -NS'4' -S'E' -tp2052 -a(dp2053 -g7 -I0 -saa(lp2054 -(S'Reinforced Alloy' -p2055 +saa(lp1906 +(S'Reactive Surface Composite' +p1907 g1099 S'1' S'I' -tp2056 -a(dp2057 +tp1908 +a(dp1909 g7 -I3 -saa(lp2058 -(g2055 +I5 +saa(lp1910 +(g1907 g1103 S'1' S'I' -tp2059 -a(dp2060 +tp1911 +a(dp1912 g7 -I30 -saa(lp2061 -(g2055 +I150 +saa(lp1913 +(g1907 g1107 S'1' S'I' -tp2062 -a(dp2063 +tp1914 +a(dp1915 g7 -I21 -saa(lp2064 -(g2055 +I60 +saa(lp1916 +(g1907 g1111 S'1' S'I' -tp2065 -a(dp2066 +tp1917 +a(dp1918 g7 -I21 -saa(lp2067 -(g2055 +I42 +saa(lp1919 +(g1907 g1115 S'1' S'I' -tp2068 -a(dp2069 +tp1920 +a(dp1921 g7 -I83 -saa(lp2070 -(g2055 +I42 +saa(lp1922 +(g1907 g1119 S'1' S'I' -tp2071 -a(dp2072 +tp1923 +a(dp1924 g7 -I14 -saa(lp2073 -(g2055 +I165 +saa(lp1925 +(g1907 g1123 S'1' S'I' -tp2074 -a(dp2075 +tp1926 +a(dp1927 g7 -I14 -saa(lp2076 -(g2055 +I27 +saa(lp1928 +(g1907 g1127 S'1' S'I' -tp2077 -a(dp2078 +tp1929 +a(dp1930 g7 -I23 -saa(lp2079 -(g2055 +I27 +saa(lp1931 +(g1907 g1131 S'1' S'I' -tp2080 -a(dp2081 +tp1932 +a(dp1933 g7 -I13 -saa(lp2082 -(g2055 +I47 +saa(lp1934 +(g1907 g1135 S'1' S'I' -tp2083 -a(dp2084 +tp1935 +a(dp1936 g7 -I32 -saa(lp2085 -(g2055 +I26 +saa(lp1937 +(g1907 g1139 S'1' S'I' -tp2086 -a(dp2087 +tp1938 +a(dp1939 g7 -I4 -saa(lp2088 -(g2055 +I63 +saa(lp1940 +(g1907 g1143 S'1' S'I' -tp2089 -a(dp2090 +tp1941 +a(dp1942 g7 -I44 -saa(lp2091 -(g2055 +I8 +saa(lp1943 +(g1907 g1147 S'1' S'I' -tp2092 -a(dp2093 +tp1944 +a(dp1945 g7 -I30 -saa(lp2094 -(g2055 +I87 +saa(lp1946 +(g1907 g1151 S'1' S'I' -tp2095 -a(dp2096 +tp1947 +a(dp1948 g7 -I44 -saa(lp2097 -(g2055 +I60 +saa(lp1949 +(g1907 g1155 S'1' S'I' -tp2098 -a(dp2099 +tp1950 +a(dp1951 g7 -I44 -saa(lp2100 -(g2055 +I87 +saa(lp1952 +(g1907 g1159 S'1' S'I' -tp2101 -a(dp2102 +tp1953 +a(dp1954 g7 -I19 -saa(lp2103 -(g2055 +I87 +saa(lp1955 +(g1907 g1163 S'1' S'I' -tp2104 -a(dp2105 +tp1956 +a(dp1957 g7 -I1 -saa(lp2106 -(g2055 +I38 +saa(lp1958 +(g1907 g1167 S'1' S'I' -tp2107 -a(dp2108 +tp1959 +a(dp1960 g7 -I30 -saa(lp2109 -(g2055 +I2 +saa(lp1961 +(g1907 g1171 S'1' S'I' -tp2110 -a(dp2111 +tp1962 +a(dp1963 g7 -I4 -saa(lp2112 -(g2055 +I60 +saa(lp1964 +(g1907 g1175 S'1' S'I' -tp2113 -a(dp2114 +tp1965 +a(dp1966 g7 -I30 -saa(lp2115 -(g2055 +I8 +saa(lp1967 +(g1907 g1179 S'1' S'I' -tp2116 -a(dp2117 +tp1968 +a(dp1969 g7 -I4 -saa(lp2118 -(g2055 +I60 +saa(lp1970 +(g1907 g1183 S'1' S'I' -tp2119 -a(dp2120 +tp1971 +a(dp1972 g7 -I12 -saa(lp2121 -(g2055 +I8 +saa(lp1973 +(g1907 g1187 S'1' S'I' -tp2122 -a(dp2123 +tp1974 +a(dp1975 g7 -I21 -saa(lp2124 -(g2055 +I23 +saa(lp1976 +(g1907 g1191 S'1' S'I' -tp2125 -a(dp2126 +tp1977 +a(dp1978 g7 -I26 -saa(lp2127 -(g2055 +I87 +saa(lp1979 +(g1907 g1195 S'1' S'I' -tp2128 -a(dp2129 +tp1980 +a(dp1981 g7 -I2 -saa(lp2130 -(g2055 +I53 +saa(lp1982 +(g1907 g1199 S'1' S'I' -tp2131 -a(dp2132 +tp1983 +a(dp1984 g7 -I75 -saa(lp2133 -(g2055 +I4 +saa(lp1985 +(g1907 g1203 S'1' S'I' -tp2134 -a(dp2135 +tp1986 +a(dp1987 g7 -I12 -saa(lp2136 -(g2055 +I150 +saa(lp1988 +(g1907 g1207 S'1' S'I' -tp2137 -a(dp2138 +tp1989 +a(dp1990 g7 -I32 -saa(lp2139 -(g2055 +I23 +saa(lp1991 +(g1907 g1211 S'1' S'I' -tp2140 -a(dp2141 +tp1992 +a(dp1993 g7 -I75 -saa(lp2142 -(g2055 +I63 +saa(lp1994 +(g1907 g1215 S'1' S'I' -tp2143 -a(dp2144 +tp1995 +a(dp1996 g7 -I5 -saa(lp2145 -(g2055 +I150 +saa(lp1997 +(g1907 g1219 S'1' S'I' -tp2146 -a(dp2147 +tp1998 +a(dp1999 g7 -I5 -saa(lp2148 -(g2055 +I9 +saa(lp2000 +(g1907 g1223 S'1' S'I' -tp2149 -a(dp2150 +tp2001 +a(dp2002 g7 -I17 -saa(lp2151 -(S'Remote Release Flak Launcher' -p2152 -NS'2' -S'B' -tp2153 -a(dp2154 +I9 +saa(lp2003 +(g1907 +g1227 +S'1' +S'I' +tp2004 +a(dp2005 g7 -I4 -saa(lp2155 -(S'Repair Limpet Controller' -p2156 +I35 +saa(lp2006 +(S'Recon Limpet Controller' +p2007 +NS'1' +S'E' +tp2008 +a(dp2009 +g7 +F1.3 +saa(lp2010 +(g2007 +NS'3' +S'E' +tp2011 +a(dp2012 +g7 +I2 +saa(lp2013 +(g2007 +NS'5' +S'E' +tp2014 +a(dp2015 +g7 +I20 +saa(lp2016 +(g2007 +NS'7' +S'E' +tp2017 +a(dp2018 +g7 +I128 +saa(lp2019 +(S'Refinery' +p2020 NS'1' S'A' +tp2021 +a(dp2022 +g7 +I0 +saa(lp2023 +(g2020 +NS'1' +S'B' +tp2024 +a(dp2025 +g7 +I0 +saa(lp2026 +(g2020 +NS'1' +S'C' +tp2027 +a(dp2028 +g7 +I0 +saa(lp2029 +(g2020 +NS'1' +S'D' +tp2030 +a(dp2031 +g7 +I0 +saa(lp2032 +(g2020 +NS'1' +S'E' +tp2033 +a(dp2034 +g7 +I0 +saa(lp2035 +(g2020 +NS'2' +S'A' +tp2036 +a(dp2037 +g7 +I0 +saa(lp2038 +(g2020 +NS'2' +S'B' +tp2039 +a(dp2040 +g7 +I0 +saa(lp2041 +(g2020 +NS'2' +S'C' +tp2042 +a(dp2043 +g7 +I0 +saa(lp2044 +(g2020 +NS'2' +S'D' +tp2045 +a(dp2046 +g7 +I0 +saa(lp2047 +(g2020 +NS'2' +S'E' +tp2048 +a(dp2049 +g7 +I0 +saa(lp2050 +(g2020 +NS'3' +S'A' +tp2051 +a(dp2052 +g7 +I0 +saa(lp2053 +(g2020 +NS'3' +S'B' +tp2054 +a(dp2055 +g7 +I0 +saa(lp2056 +(g2020 +NS'3' +S'C' +tp2057 +a(dp2058 +g7 +I0 +saa(lp2059 +(g2020 +NS'3' +S'D' +tp2060 +a(dp2061 +g7 +I0 +saa(lp2062 +(g2020 +NS'3' +S'E' +tp2063 +a(dp2064 +g7 +I0 +saa(lp2065 +(g2020 +NS'4' +S'A' +tp2066 +a(dp2067 +g7 +I0 +saa(lp2068 +(g2020 +NS'4' +S'B' +tp2069 +a(dp2070 +g7 +I0 +saa(lp2071 +(g2020 +NS'4' +S'C' +tp2072 +a(dp2073 +g7 +I0 +saa(lp2074 +(g2020 +NS'4' +S'D' +tp2075 +a(dp2076 +g7 +I0 +saa(lp2077 +(g2020 +NS'4' +S'E' +tp2078 +a(dp2079 +g7 +I0 +saa(lp2080 +(S'Reinforced Alloy' +p2081 +g1099 +S'1' +S'I' +tp2082 +a(dp2083 +g7 +I3 +saa(lp2084 +(g2081 +g1103 +S'1' +S'I' +tp2085 +a(dp2086 +g7 +I75 +saa(lp2087 +(g2081 +g1107 +S'1' +S'I' +tp2088 +a(dp2089 +g7 +I30 +saa(lp2090 +(g2081 +g1111 +S'1' +S'I' +tp2091 +a(dp2092 +g7 +I21 +saa(lp2093 +(g2081 +g1115 +S'1' +S'I' +tp2094 +a(dp2095 +g7 +I21 +saa(lp2096 +(g2081 +g1119 +S'1' +S'I' +tp2097 +a(dp2098 +g7 +I83 +saa(lp2099 +(g2081 +g1123 +S'1' +S'I' +tp2100 +a(dp2101 +g7 +I14 +saa(lp2102 +(g2081 +g1127 +S'1' +S'I' +tp2103 +a(dp2104 +g7 +I14 +saa(lp2105 +(g2081 +g1131 +S'1' +S'I' +tp2106 +a(dp2107 +g7 +I23 +saa(lp2108 +(g2081 +g1135 +S'1' +S'I' +tp2109 +a(dp2110 +g7 +I13 +saa(lp2111 +(g2081 +g1139 +S'1' +S'I' +tp2112 +a(dp2113 +g7 +I32 +saa(lp2114 +(g2081 +g1143 +S'1' +S'I' +tp2115 +a(dp2116 +g7 +I4 +saa(lp2117 +(g2081 +g1147 +S'1' +S'I' +tp2118 +a(dp2119 +g7 +I44 +saa(lp2120 +(g2081 +g1151 +S'1' +S'I' +tp2121 +a(dp2122 +g7 +I30 +saa(lp2123 +(g2081 +g1155 +S'1' +S'I' +tp2124 +a(dp2125 +g7 +I44 +saa(lp2126 +(g2081 +g1159 +S'1' +S'I' +tp2127 +a(dp2128 +g7 +I44 +saa(lp2129 +(g2081 +g1163 +S'1' +S'I' +tp2130 +a(dp2131 +g7 +I19 +saa(lp2132 +(g2081 +g1167 +S'1' +S'I' +tp2133 +a(dp2134 +g7 +I1 +saa(lp2135 +(g2081 +g1171 +S'1' +S'I' +tp2136 +a(dp2137 +g7 +I30 +saa(lp2138 +(g2081 +g1175 +S'1' +S'I' +tp2139 +a(dp2140 +g7 +I4 +saa(lp2141 +(g2081 +g1179 +S'1' +S'I' +tp2142 +a(dp2143 +g7 +I30 +saa(lp2144 +(g2081 +g1183 +S'1' +S'I' +tp2145 +a(dp2146 +g7 +I4 +saa(lp2147 +(g2081 +g1187 +S'1' +S'I' +tp2148 +a(dp2149 +g7 +I12 +saa(lp2150 +(g2081 +g1191 +S'1' +S'I' +tp2151 +a(dp2152 +g7 +I21 +saa(lp2153 +(g2081 +g1195 +S'1' +S'I' +tp2154 +a(dp2155 +g7 +I26 +saa(lp2156 +(g2081 +g1199 +S'1' +S'I' tp2157 a(dp2158 g7 -F1.3 +I2 saa(lp2159 -(g2156 -NS'1' -S'B' +(g2081 +g1203 +S'1' +S'I' tp2160 a(dp2161 g7 -I2 +I75 saa(lp2162 -(g2156 -NS'1' -S'C' +(g2081 +g1207 +S'1' +S'I' tp2163 a(dp2164 g7 -F1.3 +I12 saa(lp2165 -(g2156 -NS'1' -S'D' +(g2081 +g1211 +S'1' +S'I' tp2166 a(dp2167 g7 -F0.5 +I32 saa(lp2168 -(g2156 -NS'1' -S'E' +(g2081 +g1215 +S'1' +S'I' tp2169 a(dp2170 g7 -F1.3 +I75 saa(lp2171 -(g2156 -NS'3' -S'A' +(g2081 +g1219 +S'1' +S'I' tp2172 a(dp2173 g7 I5 saa(lp2174 -(g2156 -NS'3' -S'B' +(g2081 +g1223 +S'1' +S'I' tp2175 a(dp2176 g7 -I8 +I5 saa(lp2177 -(g2156 -NS'3' -S'C' +(g2081 +g1227 +S'1' +S'I' tp2178 a(dp2179 g7 -I5 +I17 saa(lp2180 -(g2156 -NS'3' -S'D' -tp2181 -a(dp2182 -g7 -I2 -saa(lp2183 -(g2156 -NS'3' -S'E' -tp2184 -a(dp2185 -g7 -I5 -saa(lp2186 -(g2156 -NS'5' -S'A' -tp2187 -a(dp2188 -g7 -I20 -saa(lp2189 -(g2156 -NS'5' -S'B' -tp2190 -a(dp2191 -g7 -I32 -saa(lp2192 -(g2156 -NS'5' -S'C' -tp2193 -a(dp2194 -g7 -I20 -saa(lp2195 -(g2156 -NS'5' -S'D' -tp2196 -a(dp2197 -g7 -I8 -saa(lp2198 -(g2156 -NS'5' -S'E' -tp2199 -a(dp2200 -g7 -I20 -saa(lp2201 -(g2156 -NS'7' -S'A' -tp2202 -a(dp2203 -g7 -I80 -saa(lp2204 -(g2156 -NS'7' -S'B' -tp2205 -a(dp2206 -g7 -I128 -saa(lp2207 -(g2156 -NS'7' -S'C' -tp2208 -a(dp2209 -g7 -I80 -saa(lp2210 -(g2156 -NS'7' -S'D' -tp2211 -a(dp2212 -g7 -I32 -saa(lp2213 -(g2156 -NS'7' -S'E' -tp2214 -a(dp2215 -g7 -I80 -saa(lp2216 -(S'Retributor Beam Laser' -p2217 -NS'1' -S'E' -tp2218 -a(dp2219 -g7 -I2 -saa(lp2220 -(S'Rocket Propelled FSD Disruptor' -p2221 +(S'Remote Release Flak Launcher' +p2181 NS'2' S'B' +tp2182 +a(dp2183 +g7 +I4 +saa(lp2184 +(S'Repair Limpet Controller' +p2185 +NS'1' +S'A' +tp2186 +a(dp2187 +g7 +F1.3 +saa(lp2188 +(g2185 +NS'1' +S'B' +tp2189 +a(dp2190 +g7 +I2 +saa(lp2191 +(g2185 +NS'1' +S'C' +tp2192 +a(dp2193 +g7 +F1.3 +saa(lp2194 +(g2185 +NS'1' +S'D' +tp2195 +a(dp2196 +g7 +F0.5 +saa(lp2197 +(g2185 +NS'1' +S'E' +tp2198 +a(dp2199 +g7 +F1.3 +saa(lp2200 +(g2185 +NS'3' +S'A' +tp2201 +a(dp2202 +g7 +I5 +saa(lp2203 +(g2185 +NS'3' +S'B' +tp2204 +a(dp2205 +g7 +I8 +saa(lp2206 +(g2185 +NS'3' +S'C' +tp2207 +a(dp2208 +g7 +I5 +saa(lp2209 +(g2185 +NS'3' +S'D' +tp2210 +a(dp2211 +g7 +I2 +saa(lp2212 +(g2185 +NS'3' +S'E' +tp2213 +a(dp2214 +g7 +I5 +saa(lp2215 +(g2185 +NS'5' +S'A' +tp2216 +a(dp2217 +g7 +I20 +saa(lp2218 +(g2185 +NS'5' +S'B' +tp2219 +a(dp2220 +g7 +I32 +saa(lp2221 +(g2185 +NS'5' +S'C' tp2222 a(dp2223 g7 -I4 +I20 saa(lp2224 -(S'Sensors' -p2225 -NS'1' -S'A' -tp2226 -a(dp2227 -g7 -F1.3 -saa(lp2228 -(g2225 -NS'1' -S'B' -tp2229 -a(dp2230 -g7 -I2 -saa(lp2231 -(g2225 -NS'1' -S'C' -tp2232 -a(dp2233 -g7 -F1.3 -saa(lp2234 -(g2225 -NS'1' +(g2185 +NS'5' S'D' -tp2235 -a(dp2236 +tp2225 +a(dp2226 g7 -F0.5 -saa(lp2237 -(g2225 +I8 +saa(lp2227 +(g2185 +NS'5' +S'E' +tp2228 +a(dp2229 +g7 +I20 +saa(lp2230 +(g2185 +NS'7' +S'A' +tp2231 +a(dp2232 +g7 +I80 +saa(lp2233 +(g2185 +NS'7' +S'B' +tp2234 +a(dp2235 +g7 +I128 +saa(lp2236 +(g2185 +NS'7' +S'C' +tp2237 +a(dp2238 +g7 +I80 +saa(lp2239 +(g2185 +NS'7' +S'D' +tp2240 +a(dp2241 +g7 +I32 +saa(lp2242 +(g2185 +NS'7' +S'E' +tp2243 +a(dp2244 +g7 +I80 +saa(lp2245 +(S'Research Limpet Controller' +p2246 NS'1' S'E' -tp2238 -a(dp2239 -g7 -F1.3 -saa(lp2240 -(g2225 -NS'2' -S'A' -tp2241 -a(dp2242 -g7 -F2.5 -saa(lp2243 -(g2225 -NS'2' -S'B' -tp2244 -a(dp2245 -g7 -I4 -saa(lp2246 -(g2225 -NS'2' -S'C' tp2247 a(dp2248 g7 -F2.5 +F1.3 saa(lp2249 -(g2225 -NS'2' -S'D' -tp2250 -a(dp2251 -g7 -I1 -saa(lp2252 -(g2225 -NS'2' +(S'Retributor Beam Laser' +p2250 +NS'1' S'E' -tp2253 -a(dp2254 +tp2251 +a(dp2252 g7 -F2.5 -saa(lp2255 -(g2225 -NS'3' -S'A' -tp2256 -a(dp2257 -g7 -I5 -saa(lp2258 -(g2225 -NS'3' +I2 +saa(lp2253 +(S'Rocket Propelled FSD Disruptor' +p2254 +NS'2' S'B' +tp2255 +a(dp2256 +g7 +I4 +saa(lp2257 +(S'Sensors' +p2258 +NS'1' +S'A' tp2259 a(dp2260 g7 -I8 +F1.3 saa(lp2261 -(g2225 -NS'3' -S'C' +(g2258 +NS'1' +S'B' tp2262 a(dp2263 g7 -I5 +I2 saa(lp2264 -(g2225 -NS'3' -S'D' +(g2258 +NS'1' +S'C' tp2265 a(dp2266 g7 -I2 +F1.3 saa(lp2267 -(g2225 -NS'3' -S'E' +(g2258 +NS'1' +S'D' tp2268 a(dp2269 g7 -I5 +F0.5 saa(lp2270 -(g2225 -NS'4' -S'A' +(g2258 +NS'1' +S'E' tp2271 a(dp2272 g7 -I10 +F1.3 saa(lp2273 -(g2225 -NS'4' -S'B' +(g2258 +NS'2' +S'A' tp2274 a(dp2275 g7 -I16 +F2.5 saa(lp2276 -(g2225 -NS'4' -S'C' +(g2258 +NS'2' +S'B' tp2277 a(dp2278 g7 -I10 +I4 saa(lp2279 -(g2225 -NS'4' -S'D' +(g2258 +NS'2' +S'C' tp2280 a(dp2281 g7 -I4 +F2.5 saa(lp2282 -(g2225 -NS'4' -S'E' +(g2258 +NS'2' +S'D' tp2283 a(dp2284 g7 -I10 +I1 saa(lp2285 -(g2225 -NS'5' -S'A' +(g2258 +NS'2' +S'E' tp2286 a(dp2287 g7 -I20 +F2.5 saa(lp2288 -(g2225 -NS'5' -S'B' +(g2258 +NS'3' +S'A' tp2289 a(dp2290 g7 -I32 +I5 saa(lp2291 -(g2225 -NS'5' -S'C' +(g2258 +NS'3' +S'B' tp2292 a(dp2293 g7 -I20 +I8 saa(lp2294 -(g2225 -NS'5' -S'D' +(g2258 +NS'3' +S'C' tp2295 a(dp2296 g7 -I8 +I5 saa(lp2297 -(g2225 -NS'5' -S'E' +(g2258 +NS'3' +S'D' tp2298 a(dp2299 g7 -I20 +I2 saa(lp2300 -(g2225 -NS'6' -S'A' +(g2258 +NS'3' +S'E' tp2301 a(dp2302 g7 -I40 +I5 saa(lp2303 -(g2225 -NS'6' -S'B' +(g2258 +NS'4' +S'A' tp2304 a(dp2305 g7 -I64 +I10 saa(lp2306 -(g2225 -NS'6' -S'C' +(g2258 +NS'4' +S'B' tp2307 a(dp2308 g7 -I40 +I16 saa(lp2309 -(g2225 -NS'6' -S'D' +(g2258 +NS'4' +S'C' tp2310 a(dp2311 g7 -I16 +I10 saa(lp2312 -(g2225 -NS'6' -S'E' +(g2258 +NS'4' +S'D' tp2313 a(dp2314 g7 -I40 +I4 saa(lp2315 -(g2225 -NS'7' -S'A' +(g2258 +NS'4' +S'E' tp2316 a(dp2317 g7 -I80 +I10 saa(lp2318 -(g2225 -NS'7' -S'B' +(g2258 +NS'5' +S'A' tp2319 a(dp2320 g7 -I128 +I20 saa(lp2321 -(g2225 -NS'7' -S'C' +(g2258 +NS'5' +S'B' tp2322 a(dp2323 g7 -I80 +I32 saa(lp2324 -(g2225 -NS'7' -S'D' +(g2258 +NS'5' +S'C' tp2325 a(dp2326 g7 -I32 +I20 saa(lp2327 -(g2225 -NS'7' -S'E' +(g2258 +NS'5' +S'D' tp2328 a(dp2329 g7 -I80 +I8 saa(lp2330 -(g2225 -NS'8' -S'A' +(g2258 +NS'5' +S'E' tp2331 a(dp2332 g7 -I160 +I20 saa(lp2333 -(g2225 -NS'8' -S'B' +(g2258 +NS'6' +S'A' tp2334 a(dp2335 g7 -I256 +I40 saa(lp2336 -(g2225 -NS'8' -S'C' +(g2258 +NS'6' +S'B' tp2337 a(dp2338 g7 -I160 +I64 saa(lp2339 -(g2225 -NS'8' -S'D' +(g2258 +NS'6' +S'C' tp2340 a(dp2341 g7 -I64 +I40 saa(lp2342 -(g2225 -NS'8' -S'E' +(g2258 +NS'6' +S'D' tp2343 a(dp2344 g7 -I160 +I16 saa(lp2345 +(g2258 +NS'6' +S'E' +tp2346 +a(dp2347 +g7 +I40 +saa(lp2348 +(g2258 +NS'7' +S'A' +tp2349 +a(dp2350 +g7 +I80 +saa(lp2351 +(g2258 +NS'7' +S'B' +tp2352 +a(dp2353 +g7 +I128 +saa(lp2354 +(g2258 +NS'7' +S'C' +tp2355 +a(dp2356 +g7 +I80 +saa(lp2357 +(g2258 +NS'7' +S'D' +tp2358 +a(dp2359 +g7 +I32 +saa(lp2360 +(g2258 +NS'7' +S'E' +tp2361 +a(dp2362 +g7 +I80 +saa(lp2363 +(g2258 +NS'8' +S'A' +tp2364 +a(dp2365 +g7 +I160 +saa(lp2366 +(g2258 +NS'8' +S'B' +tp2367 +a(dp2368 +g7 +I256 +saa(lp2369 +(g2258 +NS'8' +S'C' +tp2370 +a(dp2371 +g7 +I160 +saa(lp2372 +(g2258 +NS'8' +S'D' +tp2373 +a(dp2374 +g7 +I64 +saa(lp2375 +(g2258 +NS'8' +S'E' +tp2376 +a(dp2377 +g7 +I160 +saa(lp2378 (S'Shield Booster' -p2346 +p2379 NS'0' S'A' -tp2347 -a(dp2348 +tp2380 +a(dp2381 g7 F3.5 -saa(lp2349 -(g2346 +saa(lp2382 +(g2379 NS'0' S'B' -tp2350 -a(dp2351 +tp2383 +a(dp2384 g7 I3 -saa(lp2352 -(g2346 +saa(lp2385 +(g2379 NS'0' S'C' -tp2353 -a(dp2354 +tp2386 +a(dp2387 g7 I2 -saa(lp2355 -(g2346 +saa(lp2388 +(g2379 NS'0' S'D' -tp2356 -a(dp2357 +tp2389 +a(dp2390 g7 I1 -saa(lp2358 -(g2346 +saa(lp2391 +(g2379 NS'0' S'E' -tp2359 -a(dp2360 +tp2392 +a(dp2393 g7 F0.5 -saa(lp2361 +saa(lp2394 (S'Shield Cell Bank' -p2362 +p2395 NS'1' S'A' -tp2363 -a(dp2364 -g7 -F1.3 -saa(lp2365 -(g2362 -NS'1' -S'B' -tp2366 -a(dp2367 -g7 -I2 -saa(lp2368 -(g2362 -NS'1' -S'C' -tp2369 -a(dp2370 -g7 -F1.3 -saa(lp2371 -(g2362 -NS'1' -S'D' -tp2372 -a(dp2373 -g7 -F0.5 -saa(lp2374 -(g2362 -NS'1' -S'E' -tp2375 -a(dp2376 -g7 -F1.3 -saa(lp2377 -(g2362 -NS'2' -S'A' -tp2378 -a(dp2379 -g7 -F2.5 -saa(lp2380 -(g2362 -NS'2' -S'B' -tp2381 -a(dp2382 -g7 -I4 -saa(lp2383 -(g2362 -NS'2' -S'C' -tp2384 -a(dp2385 -g7 -F2.5 -saa(lp2386 -(g2362 -NS'2' -S'D' -tp2387 -a(dp2388 -g7 -I1 -saa(lp2389 -(g2362 -NS'2' -S'E' -tp2390 -a(dp2391 -g7 -F2.5 -saa(lp2392 -(g2362 -NS'3' -S'A' -tp2393 -a(dp2394 -g7 -I5 -saa(lp2395 -(g2362 -NS'3' -S'B' tp2396 a(dp2397 g7 -I8 +F1.3 saa(lp2398 -(g2362 -NS'3' -S'C' +(g2395 +NS'1' +S'B' tp2399 a(dp2400 g7 -I5 +I2 saa(lp2401 -(g2362 -NS'3' -S'D' +(g2395 +NS'1' +S'C' tp2402 a(dp2403 g7 -I2 +F1.3 saa(lp2404 -(g2362 -NS'3' -S'E' +(g2395 +NS'1' +S'D' tp2405 a(dp2406 g7 -I5 +F0.5 saa(lp2407 -(g2362 -NS'4' -S'A' +(g2395 +NS'1' +S'E' tp2408 a(dp2409 g7 -I10 +F1.3 saa(lp2410 -(g2362 -NS'4' -S'B' +(g2395 +NS'2' +S'A' tp2411 a(dp2412 g7 -I16 +F2.5 saa(lp2413 -(g2362 -NS'4' -S'C' +(g2395 +NS'2' +S'B' tp2414 a(dp2415 g7 -I10 +I4 saa(lp2416 -(g2362 -NS'4' -S'D' +(g2395 +NS'2' +S'C' tp2417 a(dp2418 g7 -I4 +F2.5 saa(lp2419 -(g2362 -NS'4' -S'E' +(g2395 +NS'2' +S'D' tp2420 a(dp2421 g7 -I10 +I1 saa(lp2422 -(g2362 -NS'5' -S'A' +(g2395 +NS'2' +S'E' tp2423 a(dp2424 g7 -I20 +F2.5 saa(lp2425 -(g2362 -NS'5' -S'B' +(g2395 +NS'3' +S'A' tp2426 a(dp2427 g7 -I32 +I5 saa(lp2428 -(g2362 -NS'5' -S'C' +(g2395 +NS'3' +S'B' tp2429 a(dp2430 g7 -I20 +I8 saa(lp2431 -(g2362 -NS'5' -S'D' +(g2395 +NS'3' +S'C' tp2432 a(dp2433 g7 -I8 +I5 saa(lp2434 -(g2362 -NS'5' -S'E' +(g2395 +NS'3' +S'D' tp2435 a(dp2436 g7 -I20 +I2 saa(lp2437 -(g2362 -NS'6' -S'A' +(g2395 +NS'3' +S'E' tp2438 a(dp2439 g7 -I40 +I5 saa(lp2440 -(g2362 -NS'6' -S'B' +(g2395 +NS'4' +S'A' tp2441 a(dp2442 g7 -I64 +I10 saa(lp2443 -(g2362 -NS'6' -S'C' +(g2395 +NS'4' +S'B' tp2444 a(dp2445 g7 -I40 +I16 saa(lp2446 -(g2362 -NS'6' -S'D' +(g2395 +NS'4' +S'C' tp2447 a(dp2448 g7 -I16 +I10 saa(lp2449 -(g2362 -NS'6' -S'E' +(g2395 +NS'4' +S'D' tp2450 a(dp2451 g7 -I40 +I4 saa(lp2452 -(g2362 -NS'7' -S'A' +(g2395 +NS'4' +S'E' tp2453 a(dp2454 g7 -I80 +I10 saa(lp2455 -(g2362 -NS'7' -S'B' +(g2395 +NS'5' +S'A' tp2456 a(dp2457 g7 -I128 +I20 saa(lp2458 -(g2362 -NS'7' -S'C' +(g2395 +NS'5' +S'B' tp2459 a(dp2460 g7 -I80 +I32 saa(lp2461 -(g2362 -NS'7' -S'D' +(g2395 +NS'5' +S'C' tp2462 a(dp2463 g7 -I32 +I20 saa(lp2464 -(g2362 -NS'7' -S'E' +(g2395 +NS'5' +S'D' tp2465 a(dp2466 g7 -I80 +I8 saa(lp2467 -(g2362 -NS'8' -S'A' +(g2395 +NS'5' +S'E' tp2468 a(dp2469 g7 -I160 +I20 saa(lp2470 -(g2362 -NS'8' -S'B' +(g2395 +NS'6' +S'A' tp2471 a(dp2472 g7 -I256 +I40 saa(lp2473 -(g2362 -NS'8' -S'C' +(g2395 +NS'6' +S'B' tp2474 a(dp2475 g7 -I160 +I64 saa(lp2476 -(g2362 -NS'8' -S'D' +(g2395 +NS'6' +S'C' tp2477 a(dp2478 g7 -I64 +I40 saa(lp2479 -(g2362 -NS'8' -S'E' +(g2395 +NS'6' +S'D' tp2480 a(dp2481 g7 -I160 +I16 saa(lp2482 +(g2395 +NS'6' +S'E' +tp2483 +a(dp2484 +g7 +I40 +saa(lp2485 +(g2395 +NS'7' +S'A' +tp2486 +a(dp2487 +g7 +I80 +saa(lp2488 +(g2395 +NS'7' +S'B' +tp2489 +a(dp2490 +g7 +I128 +saa(lp2491 +(g2395 +NS'7' +S'C' +tp2492 +a(dp2493 +g7 +I80 +saa(lp2494 +(g2395 +NS'7' +S'D' +tp2495 +a(dp2496 +g7 +I32 +saa(lp2497 +(g2395 +NS'7' +S'E' +tp2498 +a(dp2499 +g7 +I80 +saa(lp2500 +(g2395 +NS'8' +S'A' +tp2501 +a(dp2502 +g7 +I160 +saa(lp2503 +(g2395 +NS'8' +S'B' +tp2504 +a(dp2505 +g7 +I256 +saa(lp2506 +(g2395 +NS'8' +S'C' +tp2507 +a(dp2508 +g7 +I160 +saa(lp2509 +(g2395 +NS'8' +S'D' +tp2510 +a(dp2511 +g7 +I64 +saa(lp2512 +(g2395 +NS'8' +S'E' +tp2513 +a(dp2514 +g7 +I160 +saa(lp2515 (S'Shield Generator' -p2483 +p2516 NS'1' S'A' -tp2484 -a(dp2485 -g7 -F1.3 -saa(lp2486 -(g2483 -NS'2' -S'A' -tp2487 -a(dp2488 -g7 -F2.5 -saa(lp2489 -(g2483 -NS'2' -S'B' -tp2490 -a(dp2491 -g7 -I4 -saa(lp2492 -(g2483 -NS'2' -S'C' -tp2493 -a(dp2494 -g7 -F2.5 -saa(lp2495 -(g2483 -NS'2' -S'D' -tp2496 -a(dp2497 -g7 -I1 -saa(lp2498 -(g2483 -NS'2' -S'E' -tp2499 -a(dp2500 -g7 -F2.5 -saa(lp2501 -(g2483 -NS'3' -S'A' -tp2502 -a(dp2503 -g7 -I5 -saa(lp2504 -(g2483 -NS'3' -S'B' -tp2505 -a(dp2506 -g7 -I8 -saa(lp2507 -(g2483 -NS'3' -S'C' -tp2508 -a(dp2509 -g7 -I5 -saa(lp2510 -(g2483 -NS'3' -S'D' -tp2511 -a(dp2512 -g7 -I2 -saa(lp2513 -(g2483 -NS'3' -S'E' -tp2514 -a(dp2515 -g7 -I5 -saa(lp2516 -(g2483 -NS'4' -S'A' tp2517 a(dp2518 g7 -I10 +F1.3 saa(lp2519 -(g2483 -NS'4' -S'B' +(g2516 +NS'2' +S'A' tp2520 a(dp2521 g7 -I16 +F2.5 saa(lp2522 -(g2483 -NS'4' -S'C' +(g2516 +NS'2' +S'B' tp2523 a(dp2524 g7 -I10 +I4 saa(lp2525 -(g2483 -NS'4' -S'D' +(g2516 +NS'2' +S'C' tp2526 a(dp2527 g7 -I4 +F2.5 saa(lp2528 -(g2483 -NS'4' -S'E' +(g2516 +NS'2' +S'D' tp2529 a(dp2530 g7 -I10 +I1 saa(lp2531 -(g2483 -NS'5' -S'A' +(g2516 +NS'2' +S'E' tp2532 a(dp2533 g7 -I20 +F2.5 saa(lp2534 -(g2483 -NS'5' -S'B' +(g2516 +NS'3' +S'A' tp2535 a(dp2536 g7 -I32 +I5 saa(lp2537 -(g2483 -NS'5' -S'C' +(g2516 +NS'3' +S'B' tp2538 a(dp2539 g7 -I20 +I8 saa(lp2540 -(g2483 -NS'5' -S'D' +(g2516 +NS'3' +S'C' tp2541 a(dp2542 g7 -I8 +I5 saa(lp2543 -(g2483 -NS'5' -S'E' +(g2516 +NS'3' +S'D' tp2544 a(dp2545 g7 -I20 +I2 saa(lp2546 -(g2483 -NS'6' -S'A' +(g2516 +NS'3' +S'E' tp2547 a(dp2548 g7 -I40 +I5 saa(lp2549 -(g2483 -NS'6' -S'B' +(g2516 +NS'4' +S'A' tp2550 a(dp2551 g7 -I64 +I10 saa(lp2552 -(g2483 -NS'6' -S'C' +(g2516 +NS'4' +S'B' tp2553 a(dp2554 g7 -I40 +I16 saa(lp2555 -(g2483 -NS'6' -S'D' +(g2516 +NS'4' +S'C' tp2556 a(dp2557 g7 -I16 +I10 saa(lp2558 -(g2483 -NS'6' -S'E' +(g2516 +NS'4' +S'D' tp2559 a(dp2560 g7 -I40 +I4 saa(lp2561 -(g2483 -NS'7' -S'A' +(g2516 +NS'4' +S'E' tp2562 a(dp2563 g7 -I80 +I10 saa(lp2564 -(g2483 -NS'7' -S'B' +(g2516 +NS'5' +S'A' tp2565 a(dp2566 g7 -I128 +I20 saa(lp2567 -(g2483 -NS'7' -S'C' +(g2516 +NS'5' +S'B' tp2568 a(dp2569 g7 -I80 +I32 saa(lp2570 -(g2483 -NS'7' -S'D' +(g2516 +NS'5' +S'C' tp2571 a(dp2572 g7 -I32 +I20 saa(lp2573 -(g2483 -NS'7' -S'E' +(g2516 +NS'5' +S'D' tp2574 a(dp2575 g7 -I80 +I8 saa(lp2576 -(g2483 -NS'8' -S'A' +(g2516 +NS'5' +S'E' tp2577 a(dp2578 g7 -I160 +I20 saa(lp2579 -(g2483 -NS'8' -S'B' +(g2516 +NS'6' +S'A' tp2580 a(dp2581 g7 -I256 +I40 saa(lp2582 -(g2483 -NS'8' -S'C' +(g2516 +NS'6' +S'B' tp2583 a(dp2584 g7 -I160 +I64 saa(lp2585 -(g2483 -NS'8' -S'D' +(g2516 +NS'6' +S'C' tp2586 a(dp2587 g7 -I64 +I40 saa(lp2588 -(g2483 -NS'8' -S'E' +(g2516 +NS'6' +S'D' tp2589 a(dp2590 g7 -I160 +I16 saa(lp2591 -(S'Shock Mine Launcher' -p2592 -NS'1' -S'I' -tp2593 -a(dp2594 -g7 -I2 -saa(lp2595 -(S'Shutdown Field Neutraliser' -p2596 -NS'0' -S'F' -tp2597 -a(dp2598 -g7 -F1.3 -saa(lp2599 -(S'Standard Docking Computer' -p2600 -NS'1' +(g2516 +NS'6' S'E' +tp2592 +a(dp2593 +g7 +I40 +saa(lp2594 +(g2516 +NS'7' +S'A' +tp2595 +a(dp2596 +g7 +I80 +saa(lp2597 +(g2516 +NS'7' +S'B' +tp2598 +a(dp2599 +g7 +I128 +saa(lp2600 +(g2516 +NS'7' +S'C' tp2601 a(dp2602 g7 -I0 +I80 saa(lp2603 -(S'Thrusters' -p2604 -NS'2' -S'A' -tp2605 -a(dp2606 -g7 -F2.5 -saa(lp2607 -(g2604 -NS'2' -S'B' -tp2608 -a(dp2609 -g7 -I4 -saa(lp2610 -(g2604 -NS'2' -S'C' -tp2611 -a(dp2612 -g7 -F2.5 -saa(lp2613 -(g2604 -NS'2' +(g2516 +NS'7' S'D' -tp2614 -a(dp2615 +tp2604 +a(dp2605 g7 -I1 -saa(lp2616 -(g2604 -NS'2' +I32 +saa(lp2606 +(g2516 +NS'7' S'E' -tp2617 -a(dp2618 +tp2607 +a(dp2608 g7 -F2.5 -saa(lp2619 -(g2604 -NS'3' +I80 +saa(lp2609 +(g2516 +NS'8' S'A' -tp2620 -a(dp2621 +tp2610 +a(dp2611 g7 -I5 -saa(lp2622 -(g2604 -NS'3' +I160 +saa(lp2612 +(g2516 +NS'8' S'B' -tp2623 -a(dp2624 +tp2613 +a(dp2614 g7 -I8 -saa(lp2625 -(g2604 -NS'3' +I256 +saa(lp2615 +(g2516 +NS'8' S'C' +tp2616 +a(dp2617 +g7 +I160 +saa(lp2618 +(g2516 +NS'8' +S'D' +tp2619 +a(dp2620 +g7 +I64 +saa(lp2621 +(g2516 +NS'8' +S'E' +tp2622 +a(dp2623 +g7 +I160 +saa(lp2624 +(S'Shock Mine Launcher' +p2625 +NS'1' +S'I' tp2626 a(dp2627 g7 -I5 -saa(lp2628 -(g2604 -NS'3' -S'D' -tp2629 -a(dp2630 -g7 I2 -saa(lp2631 -(g2604 -NS'3' +saa(lp2628 +(S'Shutdown Field Neutraliser' +p2629 +NS'0' +S'F' +tp2630 +a(dp2631 +g7 +F1.3 +saa(lp2632 +(S'Standard Docking Computer' +p2633 +NS'1' S'E' -tp2632 -a(dp2633 +tp2634 +a(dp2635 g7 -I5 -saa(lp2634 -(g2604 -NS'4' +I0 +saa(lp2636 +(S'Thrusters' +p2637 +NS'2' S'A' -tp2635 -a(dp2636 -g7 -I10 -saa(lp2637 -(g2604 -NS'4' -S'B' tp2638 a(dp2639 g7 -I16 +F2.5 saa(lp2640 -(g2604 -NS'4' -S'C' +(g2637 +NS'2' +S'B' tp2641 a(dp2642 g7 -I10 +I4 saa(lp2643 -(g2604 -NS'4' -S'D' +(g2637 +NS'2' +S'C' tp2644 a(dp2645 g7 -I4 +F2.5 saa(lp2646 -(g2604 -NS'4' -S'E' +(g2637 +NS'2' +S'D' tp2647 a(dp2648 g7 -I10 +I1 saa(lp2649 -(g2604 -NS'5' -S'A' +(g2637 +NS'2' +S'E' tp2650 a(dp2651 g7 -I20 +F2.5 saa(lp2652 -(g2604 -NS'5' -S'B' +(g2637 +NS'3' +S'A' tp2653 a(dp2654 g7 -I32 +I5 saa(lp2655 -(g2604 -NS'5' -S'C' +(g2637 +NS'3' +S'B' tp2656 a(dp2657 g7 -I20 +I8 saa(lp2658 -(g2604 -NS'5' -S'D' +(g2637 +NS'3' +S'C' tp2659 a(dp2660 g7 -I8 +I5 saa(lp2661 -(g2604 -NS'5' -S'E' +(g2637 +NS'3' +S'D' tp2662 a(dp2663 g7 -I20 +I2 saa(lp2664 -(g2604 -NS'6' -S'A' +(g2637 +NS'3' +S'E' tp2665 a(dp2666 g7 -I40 +I5 saa(lp2667 -(g2604 -NS'6' -S'B' +(g2637 +NS'4' +S'A' tp2668 a(dp2669 g7 -I64 +I10 saa(lp2670 -(g2604 -NS'6' -S'C' +(g2637 +NS'4' +S'B' tp2671 a(dp2672 g7 -I40 +I16 saa(lp2673 -(g2604 -NS'6' -S'D' +(g2637 +NS'4' +S'C' tp2674 a(dp2675 g7 -I16 +I10 saa(lp2676 -(g2604 -NS'6' -S'E' +(g2637 +NS'4' +S'D' tp2677 a(dp2678 g7 -I40 +I4 saa(lp2679 -(g2604 -NS'7' -S'A' +(g2637 +NS'4' +S'E' tp2680 a(dp2681 g7 -I80 +I10 saa(lp2682 -(g2604 -NS'7' -S'B' +(g2637 +NS'5' +S'A' tp2683 a(dp2684 g7 -I128 +I20 saa(lp2685 -(g2604 -NS'7' -S'C' +(g2637 +NS'5' +S'B' tp2686 a(dp2687 g7 -I80 +I32 saa(lp2688 -(g2604 -NS'7' -S'D' +(g2637 +NS'5' +S'C' tp2689 a(dp2690 g7 -I32 +I20 saa(lp2691 -(g2604 -NS'7' -S'E' +(g2637 +NS'5' +S'D' tp2692 a(dp2693 g7 -I80 +I8 saa(lp2694 -(g2604 -NS'8' -S'A' +(g2637 +NS'5' +S'E' tp2695 a(dp2696 g7 -I160 +I20 saa(lp2697 -(g2604 -NS'8' -S'B' +(g2637 +NS'6' +S'A' tp2698 a(dp2699 g7 -I256 +I40 saa(lp2700 -(g2604 -NS'8' -S'C' +(g2637 +NS'6' +S'B' tp2701 a(dp2702 g7 -I160 +I64 saa(lp2703 -(g2604 -NS'8' -S'D' +(g2637 +NS'6' +S'C' tp2704 a(dp2705 g7 -I64 +I40 saa(lp2706 -(g2604 -NS'8' -S'E' +(g2637 +NS'6' +S'D' tp2707 a(dp2708 g7 -I160 +I16 saa(lp2709 +(g2637 +NS'6' +S'E' +tp2710 +a(dp2711 +g7 +I40 +saa(lp2712 +(g2637 +NS'7' +S'A' +tp2713 +a(dp2714 +g7 +I80 +saa(lp2715 +(g2637 +NS'7' +S'B' +tp2716 +a(dp2717 +g7 +I128 +saa(lp2718 +(g2637 +NS'7' +S'C' +tp2719 +a(dp2720 +g7 +I80 +saa(lp2721 +(g2637 +NS'7' +S'D' +tp2722 +a(dp2723 +g7 +I32 +saa(lp2724 +(g2637 +NS'7' +S'E' +tp2725 +a(dp2726 +g7 +I80 +saa(lp2727 +(g2637 +NS'8' +S'A' +tp2728 +a(dp2729 +g7 +I160 +saa(lp2730 +(g2637 +NS'8' +S'B' +tp2731 +a(dp2732 +g7 +I256 +saa(lp2733 +(g2637 +NS'8' +S'C' +tp2734 +a(dp2735 +g7 +I160 +saa(lp2736 +(g2637 +NS'8' +S'D' +tp2737 +a(dp2738 +g7 +I64 +saa(lp2739 +(g2637 +NS'8' +S'E' +tp2740 +a(dp2741 +g7 +I160 +saa(lp2742 (S'Torpedo Pylon' -p2710 +p2743 NS'1' S'I' -tp2711 -a(dp2712 +tp2744 +a(dp2745 g7 I2 -saa(lp2713 -(g2710 +saa(lp2746 +(g2743 NS'2' S'I' -tp2714 -a(dp2715 +tp2747 +a(dp2748 g7 I4 -saa(lp2716 +saa(lp2749 (S'Xeno Scanner' -p2717 +p2750 NS'0' S'E' -tp2718 -a(dp2719 +tp2751 +a(dp2752 g7 F1.3 -saatRp2720 +saatRp2753 . \ No newline at end of file diff --git a/outfitting.py b/outfitting.py index a122a313..83088b86 100644 --- a/outfitting.py +++ b/outfitting.py @@ -261,6 +261,7 @@ internal_map = { 'passengercabin' : 'Passenger Cabin', 'prospector' : 'Prospector Limpet Controller', 'refinery' : 'Refinery', + 'recon' : 'Recon Limpet Controller', 'repair' : 'Repair Limpet Controller', 'repairer' : 'Auto Field-Maintenance Unit', 'resourcesiphon' : 'Hatch Breaker Limpet Controller', diff --git a/ships.p b/ships.p index ae865753..b2af170c 100644 --- a/ships.p +++ b/ships.p @@ -10,190 +10,196 @@ S'hullMass' p6 I35 saa(lp7 -S'Anaconda' +S'Alliance Chieftain' p8 a(dp9 g6 -I400 +I420 saa(lp10 -S'Asp Explorer' +S'Anaconda' p11 a(dp12 g6 -I280 +I400 saa(lp13 -S'Asp Scout' +S'Asp Explorer' p14 a(dp15 g6 -I150 +I280 saa(lp16 -S'Beluga Liner' +S'Asp Scout' p17 a(dp18 g6 -I950 +I150 saa(lp19 -S'Cobra MkIII' +S'Beluga Liner' p20 a(dp21 g6 -I180 +I950 saa(lp22 -S'Cobra MkIV' +S'Cobra MkIII' p23 a(dp24 g6 -I210 +I180 saa(lp25 -S'Diamondback Explorer' +S'Cobra MkIV' p26 a(dp27 g6 -I260 +I210 saa(lp28 -S'Diamondback Scout' +S'Diamondback Explorer' p29 a(dp30 g6 -I170 +I260 saa(lp31 -S'Dolphin' +S'Diamondback Scout' p32 a(dp33 g6 -I140 +I170 saa(lp34 -S'Eagle' +S'Dolphin' p35 a(dp36 g6 -I50 +I140 saa(lp37 -S'Federal Assault Ship' +S'Eagle' p38 a(dp39 g6 -I480 +I50 saa(lp40 -S'Federal Corvette' +S'Federal Assault Ship' p41 a(dp42 g6 -I900 +I480 saa(lp43 -S'Federal Dropship' +S'Federal Corvette' p44 a(dp45 g6 -I580 +I900 saa(lp46 -S'Federal Gunship' +S'Federal Dropship' p47 a(dp48 g6 I580 saa(lp49 -S'Fer-de-Lance' +S'Federal Gunship' p50 a(dp51 g6 -I250 +I580 saa(lp52 -S'Hauler' +S'Fer-de-Lance' p53 a(dp54 g6 -I14 +I250 saa(lp55 -S'Imperial Clipper' +S'Hauler' p56 a(dp57 g6 -I400 +I14 saa(lp58 -S'Imperial Courier' +S'Imperial Clipper' p59 a(dp60 g6 -I35 +I400 saa(lp61 -S'Imperial Cutter' +S'Imperial Courier' p62 a(dp63 g6 -I1100 +I35 saa(lp64 -S'Imperial Eagle' +S'Imperial Cutter' p65 a(dp66 g6 -I50 +I1100 saa(lp67 -S'Keelback' +S'Imperial Eagle' p68 a(dp69 g6 -I180 +I50 saa(lp70 -S'Orca' +S'Keelback' p71 a(dp72 g6 -I290 +I180 saa(lp73 -S'Python' +S'Orca' p74 a(dp75 g6 -I350 +I290 saa(lp76 -S'Sidewinder' +S'Python' p77 a(dp78 g6 -I25 +I350 saa(lp79 -S'Type-10 Defender' +S'Sidewinder' p80 a(dp81 g6 -I1200 +I25 saa(lp82 -S'Type-6 Transporter' +S'Type-10 Defender' p83 a(dp84 g6 -I155 +I1200 saa(lp85 -S'Type-7 Transporter' +S'Type-6 Transporter' p86 a(dp87 g6 -I420 +I155 saa(lp88 -S'Type-9 Heavy' +S'Type-7 Transporter' p89 a(dp90 g6 -I1000 +I420 saa(lp91 -S'Viper MkIII' +S'Type-9 Heavy' p92 a(dp93 g6 -I50 +I850 saa(lp94 -S'Viper MkIV' +S'Viper MkIII' p95 a(dp96 g6 -I190 +I50 saa(lp97 -S'Vulture' +S'Viper MkIV' p98 a(dp99 g6 +I190 +saa(lp100 +S'Vulture' +p101 +a(dp102 +g6 I230 -saatRp100 +saatRp103 . \ No newline at end of file From a081b6b637ab86057660e89f4efe9475933da26d Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Mon, 29 Jan 2018 01:34:30 +0000 Subject: [PATCH 02/15] Monitor player status, and call new "status" plugin callback --- EDMarketConnector.py | 19 +++++++ PLUGINS.md | 14 ++++- plug.py | 21 +++++++ status.py | 131 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 status.py diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 672ab03c..1a1fd7e7 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -57,6 +57,7 @@ import prefs import plug from hotkey import hotkeymgr from monitor import monitor +from status import status from theme import theme @@ -269,6 +270,7 @@ class AppWindow: self.w.bind('<KP_Enter>', self.getandsend) self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring + self.w.bind_all('<<StatusEvent>>', self.status_event) # Cmdr Status monitoring self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar self.w.bind_all('<<Quit>>', self.onexit) # Updater @@ -627,6 +629,11 @@ class AppWindow: if not config.getint('hotkey_mute'): hotkeymgr.play_bad() + if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: + # Can start status monitoring + if not status.start(self.w, monitor.started): + print "Can't start Status monitoring" + # Don't send to EDDN while on crew if monitor.state['Captain']: return @@ -678,6 +685,17 @@ class AppWindow: if not config.getint('hotkey_mute'): hotkeymgr.play_bad() + # Handle Status event + def status_event(self, event): + entry = status.status + if entry: + # Currently we don't do anything with these events + err = plug.notify_status(monitor.cmdr, monitor.is_beta, entry) + if err: + self.status['text'] = err + if not config.getint('hotkey_mute'): + hotkeymgr.play_bad() + # Display asynchronous error from plugin def plugin_error(self, event=None): if plug.last_error.get('msg'): @@ -779,6 +797,7 @@ class AppWindow: config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen hotkeymgr.unregister() + status.close() monitor.close() plug.notify_stop() self.eddn.close() diff --git a/PLUGINS.md b/PLUGINS.md index f1c5ef8f..c85a5fe5 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -106,7 +106,7 @@ this.status["text"] = "Happy!" ## Events -Once you have created your plugin and EDMC has loaded it there are two other functions you can define to be notified by EDMC when something happens: `journal_entry()` and `cmdr_data()`. +Once you have created your plugin and EDMC has loaded it there are three other functions you can define to be notified by EDMC when something happens: `journal_entry()`, `status()` and `cmdr_data()`. Your events all get called on the main tkinter loop so be sure not to block for very long or the EDMC will appear to freeze. If you have a long running operation then you should take a look at how to do background updates in tkinter - http://effbot.org/zone/tkinter-threads.htm @@ -128,6 +128,16 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) ``` +### Player Status + +This gets called periodically - typically about once a second - whith the players live status + +```python +def status(cmdr, is_beta, entry): + deployed = entry['Flags'] & 1<<6 + sys.stderr.write("Hardpoints {}\n", deployed and "deployed" or "stowed") +``` + ### Getting Commander Data This gets called when EDMC has just fetched fresh Cmdr and station data from Frontier's servers. @@ -144,7 +154,7 @@ The data is a dictionary and full of lots of wonderful stuff! ## Error messages -You can display an error in EDMC's status area by returning a string from your `journal_entry()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound). +You can display an error in EDMC's status area by returning a string from your `journal_entry()`, `status()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound). The status area is shared between EDMC itself and all other plugins, so your message won't be displayed for very long. Create a dedicated widget if you need to display routine status information. diff --git a/plug.py b/plug.py index 9ffc29a6..045a59f3 100644 --- a/plug.py +++ b/plug.py @@ -226,6 +226,27 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state): return error +def notify_status(cmdr, is_beta, entry): + """ + Send a status entry to each plugin. + :param cmdr: The piloting Cmdr name + :param is_beta: whether the player is in a Beta universe. + :param entry: The status entry as a dictionary + :return: Error message from the first plugin that returns one (if any) + """ + error = None + for plugin in PLUGINS: + status = plugin._get_func('status') + if status: + try: + # Pass a copy of the status entry in case the callee modifies it + newerror = status(cmdr, is_beta, dict(entry)) + error = error or newerror + except: + print_exc() + return error + + def notify_system_changed(timestamp, system, coordinates): """ Send notification data to each plugin when we arrive at a new system. diff --git a/status.py b/status.py new file mode 100644 index 00000000..466b30fe --- /dev/null +++ b/status.py @@ -0,0 +1,131 @@ +import json +from calendar import timegm +from operator import itemgetter +from os import listdir, stat +from os.path import getmtime, isdir, join +from sys import platform +import time + +if __debug__: + from traceback import print_exc + +from config import config + + +if platform=='darwin': + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler + +elif platform=='win32': + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler + +else: + # Linux's inotify doesn't work over CIFS or NFS, so poll + FileSystemEventHandler = object # dummy + + +# Status.json handler +class Status(FileSystemEventHandler): + + _POLL = 1 # Fallback polling interval + + def __init__(self): + FileSystemEventHandler.__init__(self) # futureproofing - not need for current version of watchdog + self.root = None + self.currentdir = None # The actual logdir that we're monitoring + self.observer = None + self.observed = None # a watchdog ObservedWatch, or None if polling + self.status = {} # Current status for communicating status back to main thread + + def start(self, root, started): + self.root = root + self.session_start = started + + logdir = config.get('journaldir') or config.default_journal_dir + if not logdir or not isdir(logdir): + self.stop() + return False + + if self.currentdir and self.currentdir != logdir: + self.stop() + self.currentdir = logdir + + # Set up a watchdog observer. + # File system events are unreliable/non-existent over network drives on Linux. + # We can't easily tell whether a path points to a network drive, so assume + # any non-standard logdir might be on a network drive and poll instead. + polling = bool(config.get('statusdir')) and platform != 'win32' + if not polling and not self.observer: + self.observer = Observer() + self.observer.daemon = True + self.observer.start() + elif polling and self.observer: + self.observer.stop() + self.observer = None + + if not self.observed and not polling: + self.observed = self.observer.schedule(self, self.currentdir) + + if __debug__: + print '%s status "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) + + # Even if we're not intending to poll, poll at least once to process pre-existing + # data and to check whether the watchdog thread has crashed due to events not + # being supported on this filesystem. + self.root.after(self._POLL * 1000/2, self.poll, True) + + return True + + def stop(self): + if __debug__: + print 'Stopping monitoring Status' + self.currentdir = None + if self.observed: + self.observed = None + self.observer.unschedule_all() + self.status = {} + + def close(self): + self.stop() + if self.observer: + self.observer.stop() + if self.observer: + self.observer.join() + self.observer = None + + def poll(self, first_time=False): + if not self.currentdir: + # Stopped + self.status = {} + else: + self.process() + + if first_time: + # Watchdog thread + emitter = self.observed and self.observer._emitter_for_watch[self.observed] # Note: Uses undocumented attribute + if emitter and emitter.is_alive(): + return # Watchdog thread still running - stop polling + + self.root.after(self._POLL * 1000, self.poll) # keep polling + + def on_modified(self, event): + # watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows + if event.is_directory or stat(event.src_path).st_size: # Can get on_modified events when the file is emptied + self.process(event.src_path if not event.is_directory else None) + + # Can be called either in watchdog thread or, if polling, in main thread. + def process(self, logfile=None): + try: + with open(join(self.currentdir, 'Status.json'), 'rb') as h: + entry = json.load(h) + + # Status file is shared between beta and live. So filter out status not in this game session. + if timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) >= self.session_start and self.status != entry: + self.status = entry + self.root.event_generate('<<StatusEvent>>', when="tail") + except: + if __debug__: print_exc() + +# singleton +status = Status() From 75edff5fc3c45ba2bce7b9f17b96c12ddc49a871 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Mon, 29 Jan 2018 23:39:00 +0000 Subject: [PATCH 03/15] Rename plugin callback to dashboard_entry --- EDMarketConnector.py | 16 ++++++++-------- PLUGINS.md | 8 ++++---- status.py => dashboard.py | 14 +++++++------- plug.py | 4 ++-- 4 files changed, 21 insertions(+), 21 deletions(-) rename status.py => dashboard.py (91%) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 1a1fd7e7..aed8b007 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -57,7 +57,7 @@ import prefs import plug from hotkey import hotkeymgr from monitor import monitor -from status import status +from dashboard import dashboard from theme import theme @@ -270,7 +270,7 @@ class AppWindow: self.w.bind('<KP_Enter>', self.getandsend) self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring - self.w.bind_all('<<StatusEvent>>', self.status_event) # Cmdr Status monitoring + self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar self.w.bind_all('<<Quit>>', self.onexit) # Updater @@ -630,8 +630,8 @@ class AppWindow: hotkeymgr.play_bad() if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: - # Can start status monitoring - if not status.start(self.w, monitor.started): + # Can start dashboard monitoring + if not dashboard.start(self.w, monitor.started): print "Can't start Status monitoring" # Don't send to EDDN while on crew @@ -686,11 +686,11 @@ class AppWindow: hotkeymgr.play_bad() # Handle Status event - def status_event(self, event): - entry = status.status + def dashboard_event(self, event): + entry = dashboard.status if entry: # Currently we don't do anything with these events - err = plug.notify_status(monitor.cmdr, monitor.is_beta, entry) + err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry) if err: self.status['text'] = err if not config.getint('hotkey_mute'): @@ -797,7 +797,7 @@ class AppWindow: config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen hotkeymgr.unregister() - status.close() + dashboard.close() monitor.close() plug.notify_stop() self.eddn.close() diff --git a/PLUGINS.md b/PLUGINS.md index c85a5fe5..663d0140 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -128,12 +128,12 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): sys.stderr.write("Arrived at {}\n".format(entry['StarSystem'])) ``` -### Player Status +### Player Dashboard -This gets called periodically - typically about once a second - whith the players live status +This gets called when something on the player's cockpit display changes - typically about once a second when in orbital flight ```python -def status(cmdr, is_beta, entry): +def dashboard_entry(cmdr, is_beta, entry): deployed = entry['Flags'] & 1<<6 sys.stderr.write("Hardpoints {}\n", deployed and "deployed" or "stowed") ``` @@ -154,7 +154,7 @@ The data is a dictionary and full of lots of wonderful stuff! ## Error messages -You can display an error in EDMC's status area by returning a string from your `journal_entry()`, `status()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound). +You can display an error in EDMC's status area by returning a string from your `journal_entry()`, `status_entry()` or `cmdr_data()` function, or asynchronously (e.g. from a "worker" thread that is performing a long running operation) by calling `plug.show_error()`. Either method will cause the "bad" sound to be played (unless the user has muted sound). The status area is shared between EDMC itself and all other plugins, so your message won't be displayed for very long. Create a dedicated widget if you need to display routine status information. diff --git a/status.py b/dashboard.py similarity index 91% rename from status.py rename to dashboard.py index 466b30fe..ce78f02d 100644 --- a/status.py +++ b/dashboard.py @@ -2,7 +2,7 @@ import json from calendar import timegm from operator import itemgetter from os import listdir, stat -from os.path import getmtime, isdir, join +from os.path import isdir, join from sys import platform import time @@ -26,7 +26,7 @@ else: # Status.json handler -class Status(FileSystemEventHandler): +class Dashboard(FileSystemEventHandler): _POLL = 1 # Fallback polling interval @@ -55,7 +55,7 @@ class Status(FileSystemEventHandler): # File system events are unreliable/non-existent over network drives on Linux. # We can't easily tell whether a path points to a network drive, so assume # any non-standard logdir might be on a network drive and poll instead. - polling = bool(config.get('statusdir')) and platform != 'win32' + polling = bool(config.get('journaldir')) and platform != 'win32' if not polling and not self.observer: self.observer = Observer() self.observer.daemon = True @@ -68,7 +68,7 @@ class Status(FileSystemEventHandler): self.observed = self.observer.schedule(self, self.currentdir) if __debug__: - print '%s status "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) + print '%s Dashboard "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir) # Even if we're not intending to poll, poll at least once to process pre-existing # data and to check whether the watchdog thread has crashed due to events not @@ -79,7 +79,7 @@ class Status(FileSystemEventHandler): def stop(self): if __debug__: - print 'Stopping monitoring Status' + print 'Stopping monitoring Dashboard' self.currentdir = None if self.observed: self.observed = None @@ -123,9 +123,9 @@ class Status(FileSystemEventHandler): # Status file is shared between beta and live. So filter out status not in this game session. if timegm(time.strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')) >= self.session_start and self.status != entry: self.status = entry - self.root.event_generate('<<StatusEvent>>', when="tail") + self.root.event_generate('<<DashboardEvent>>', when="tail") except: if __debug__: print_exc() # singleton -status = Status() +dashboard = Dashboard() diff --git a/plug.py b/plug.py index 045a59f3..8ee5c29c 100644 --- a/plug.py +++ b/plug.py @@ -226,7 +226,7 @@ def notify_journal_entry(cmdr, is_beta, system, station, entry, state): return error -def notify_status(cmdr, is_beta, entry): +def notify_dashboard_entry(cmdr, is_beta, entry): """ Send a status entry to each plugin. :param cmdr: The piloting Cmdr name @@ -236,7 +236,7 @@ def notify_status(cmdr, is_beta, entry): """ error = None for plugin in PLUGINS: - status = plugin._get_func('status') + status = plugin._get_func('dashboard_entry') if status: try: # Pass a copy of the status entry in case the callee modifies it From 1636148fe07c7b33dde82b7030f1eaeaa1be485c Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Tue, 30 Jan 2018 22:56:47 +0000 Subject: [PATCH 04/15] Include planetary body in StartUp event --- monitor.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/monitor.py b/monitor.py index f1358791..392be811 100644 --- a/monitor.py +++ b/monitor.py @@ -224,24 +224,18 @@ class EDLogs(FileSystemEventHandler): if self.live: if self.game_was_running: # Game is running locally + entry = OrderedDict([ + ('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())), + ('event', 'StartUp'), + ('StarSystem', self.system), + ('StarPos', self.coordinates), + ]) + if self.body: + entry['Body'] = self.body + entry['Docked'] = bool(self.station) if self.station: - entry = OrderedDict([ - ('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())), - ('event', 'StartUp'), - ('Docked', True), - ('StationName', self.station), - ('StationType', self.stationtype), - ('StarSystem', self.system), - ('StarPos', self.coordinates), - ]) - else: - entry = OrderedDict([ - ('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())), - ('event', 'StartUp'), - ('Docked', False), - ('StarSystem', self.system), - ('StarPos', self.coordinates), - ]) + entry['StationName'] = self.station + entry['StationType'] = self.stationtype self.event_queue.append(json.dumps(entry, separators=(', ', ':'))) else: self.event_queue.append(None) # Generate null event to update the display (with possibly out-of-date info) @@ -403,9 +397,11 @@ class EDLogs(FileSystemEventHandler): entry.get('StationName')) # May be None self.stationtype = entry.get('StationType') # May be None self.stationservices = entry.get('StationServices') # None under E:D < 2.4 + elif entry['event'] == 'ApproachBody': + self.planet = entry['Body'] elif entry['event'] == 'SupercruiseExit': self.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None - elif entry['event'] == 'SupercruiseEntry': + elif entry['event'] in ['LeaveBody', 'SupercruiseEntry']: self.planet = None elif entry['event'] in ['Rank', 'Promotion']: for k,v in entry.iteritems(): From 7b8afa4f4a67de358d5db4d1096b942f1a845a77 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Tue, 30 Jan 2018 23:07:14 +0000 Subject: [PATCH 05/15] Fix StartUp event Bug introduced in 1636148 --- monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monitor.py b/monitor.py index 392be811..9b626681 100644 --- a/monitor.py +++ b/monitor.py @@ -230,8 +230,8 @@ class EDLogs(FileSystemEventHandler): ('StarSystem', self.system), ('StarPos', self.coordinates), ]) - if self.body: - entry['Body'] = self.body + if self.planet: + entry['Body'] = self.planet entry['Docked'] = bool(self.station) if self.station: entry['StationName'] = self.station From 1a2b16a7b39e37f0d60b39df03d4b1df46676b0f Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sat, 10 Feb 2018 16:43:08 +0000 Subject: [PATCH 06/15] Handle transitory files --- dashboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard.py b/dashboard.py index ce78f02d..35a3f7ef 100644 --- a/dashboard.py +++ b/dashboard.py @@ -2,7 +2,7 @@ import json from calendar import timegm from operator import itemgetter from os import listdir, stat -from os.path import isdir, join +from os.path import isdir, isfile, join from sys import platform import time @@ -111,7 +111,7 @@ class Dashboard(FileSystemEventHandler): def on_modified(self, event): # watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows - if event.is_directory or stat(event.src_path).st_size: # Can get on_modified events when the file is emptied + if event.is_directory or (isfile(event.src_path) and stat(event.src_path).st_size): # Can get on_modified events when the file is emptied self.process(event.src_path if not event.is_directory else None) # Can be called either in watchdog thread or, if polling, in main thread. From 4e674f85d58e961ca432d7da40253a85e5847380 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sat, 10 Feb 2018 17:09:32 +0000 Subject: [PATCH 07/15] Log individual event errors --- plugins/edsm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/edsm.py b/plugins/edsm.py index 870c9407..d4ce4d05 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -310,14 +310,14 @@ def worker(): if msgnum // 100 == 2: print('EDSM\t%s %s\t%s' % (msgnum, msg, json.dumps(pending, separators = (',', ': ')))) plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg)) - elif not closing: - # Update main window's system status - for i in range(len(pending) - 1, -1, -1): - if pending[i]['event'] in ['StartUp', 'Location', 'FSDJump']: - this.lastlookup = reply['events'][i] + else: + for e, r in zip(pending, reply): + if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump']: + # Update main window's system status + this.lastlookup = r this.system.event_generate('<<EDSMStatus>>', when="tail") # calls update_status in main thread - break - + elif r['msgnum'] // 100 != 1: + print('EDSM\t%s %s\t%s' % (r['msgnum'], r['msg'], json.dumps(e, separators = (',', ': ')))) pending = [] break From d8dd7af2e22db2e426a91ebff04d6562dd204439 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sat, 10 Feb 2018 17:10:03 +0000 Subject: [PATCH 08/15] Adjust batching for new startup event order --- monitor.py | 4 ++-- plugins/edsm.py | 38 ++++++++++++++++++++++++-------------- plugins/inara.py | 14 +++++++------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/monitor.py b/monitor.py index 9b626681..b60eca34 100644 --- a/monitor.py +++ b/monitor.py @@ -328,8 +328,9 @@ class EDLogs(FileSystemEventHandler): 'ShipName' : None, 'ShipType' : None, } + elif entry['event'] == 'Commander': + self.live = True # First event in 3.0 elif entry['event'] == 'LoadGame': - self.live = True self.cmdr = entry['Commander'] self.mode = entry.get('GameMode') # 'Open', 'Solo', 'Group', or None for CQC (and Training - but no LoadGame event) self.group = entry.get('Group') @@ -413,7 +414,6 @@ class EDLogs(FileSystemEventHandler): self.state['Rank'][k] = (self.state['Rank'][k][0], min(v, 100)) # perhaps not taken promotion mission yet elif entry['event'] == 'Cargo': - self.live = True # First event in 2.3 self.state['Cargo'] = defaultdict(int) self.state['Cargo'].update({ self.canonicalise(x['Name']): x['Count'] for x in entry['Inventory'] }) elif entry['event'] in ['CollectCargo', 'MarketBuy', 'BuyDrones', 'MiningRefined']: diff --git a/plugins/edsm.py b/plugins/edsm.py index d4ce4d05..5cb0b824 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -39,6 +39,8 @@ this.lastlookup = False # whether the last lookup succeeded # Game state this.multicrew = False # don't send captain's ship info to EDSM while on a crew this.coordinates = None +this.newgame = False # starting up - batch initial burst of events +this.newgame_docked = False # starting up while docked def plugin_start(): # Can't be earlier since can only call PhotoImage after window is created @@ -198,6 +200,16 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): elif entry['event'] == 'LoadGame': this.coordinates = None + if entry['event'] in ['LoadGame', 'Commander', 'NewCommander']: + this.newgame = True + this.newgame_docked = False + elif entry['event'] == 'StartUp': + this.newgame = False + this.newgame_docked = False + elif entry['event'] == 'Location': + this.newgame = True + this.newgame_docked = entry.get('Docked', False) + # Send interesting events to EDSM if config.getint('edsm_out') and not is_beta and not this.multicrew and credentials(cmdr) and entry['event'] not in this.discardedEvents: # Introduce transient states into the event @@ -210,15 +222,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): entry.update(transient) if entry['event'] == 'LoadGame': - # Synthesise Cargo and Materials events on LoadGame since we will have missed them because Cmdr was unknown - cargo = { - 'timestamp': entry['timestamp'], - 'event': 'Cargo', - 'Inventory': [ { 'Name': k, 'Count': v } for k,v in state['Cargo'].iteritems() ], - } - cargo.update(transient) - this.queue.put((cmdr, cargo)) - + # Synthesise Materials events on LoadGame since we will have missed it materials = { 'timestamp': entry['timestamp'], 'event': 'Materials', @@ -311,7 +315,7 @@ def worker(): print('EDSM\t%s %s\t%s' % (msgnum, msg, json.dumps(pending, separators = (',', ': ')))) plug.show_error(_('Error: EDSM {MSG}').format(MSG=msg)) else: - for e, r in zip(pending, reply): + for e, r in zip(pending, reply['events']): if not closing and e['event'] in ['StartUp', 'Location', 'FSDJump']: # Update main window's system status this.lastlookup = r @@ -337,10 +341,16 @@ def worker(): # Whether any of the entries should be sent immediately def should_send(entries): for entry in entries: - if (entry['event'] not in ['CommunityGoal', # Spammed periodically - 'Cargo', 'Loadout', 'Materials', 'LoadGame', 'Rank', 'Progress', # Will be followed by 'Docked' or 'Location' - 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap'] and # " - not (entry['event'] == 'Location' and entry.get('Docked'))): # " + if (entry['event'] == 'Cargo' and not this.newgame_docked) or entry['event'] == 'Docked': + # Cargo is the last event on startup, unless starting when docked in which case Docked is the last event + this.newgame = False + this.newgame_docked = False + return True + elif this.newgame: + pass + elif entry['event'] not in ['CommunityGoal', # Spammed periodically + 'ModuleBuy', 'ModuleSell', 'ModuleSwap', # will be shortly followed by "Loadout" + 'ShipyardBuy', 'ShipyardNew', 'ShipyardSwap']: # " return True return False diff --git a/plugins/inara.py b/plugins/inara.py index d0d731f2..e5392f6b 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -36,7 +36,7 @@ this.cmdr = None this.multicrew = False # don't send captain's ship info to Inara while on a crew this.newuser = False # just entered API Key this.undocked = False # just undocked -this.suppress_docked = False # Skip Docked event after Location if started docked +this.suppress_docked = False # Skip initial Docked event if started docked this.cargo = None this.materials = None this.lastcredits = 0 # Send credit update soon after Startup / new game @@ -163,7 +163,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): this.suppress_docked = True - # Send location and status on new game or StartUp. Assumes Location is the last event on a new game (other than Docked). + # Send location and status on new game or StartUp. Assumes Cargo is the last event on a new game (other than Docked). # Always send an update on Docked, FSDJump, Undocked+SuperCruise, Promotion and EngineerProgress. # Also send material and cargo (if changed) whenever we send an update. @@ -172,7 +172,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): old_events = len(this.events) # Will only send existing events if we add a new event below # Send rank info to Inara on startup or change - if (entry['event'] in ['StartUp', 'Location'] or this.newuser) and state['Rank']: + if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser): for k,v in state['Rank'].iteritems(): if v is not None: add_event('setCommanderRankPilot', entry['timestamp'], @@ -227,7 +227,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): ])) # Update ship - if (entry['event'] in ['StartUp', 'Location', 'ShipyardNew'] or + if (entry['event'] in ['StartUp', 'Cargo'] or (entry['event'] == 'Loadout' and this.shipswap) or this.newuser): if entry['event'] == 'ShipyardNew': @@ -247,7 +247,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): this.shipswap = False # Update location - if (entry['event'] in ['StartUp', 'Location'] or this.newuser) and system: + if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser) and system: this.undocked = False add_event('setCommanderTravelLocation', entry['timestamp'], OrderedDict([ @@ -262,7 +262,7 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): # Undocked and now docking again. Don't send. this.undocked = False elif this.suppress_docked: - # Don't send Docked event on new game - i.e. following 'Location' event + # Don't send initial Docked event on new game this.suppress_docked = False else: add_event('addCommanderTravelDock', entry['timestamp'], @@ -603,7 +603,7 @@ def worker(): # Log individual errors and warnings for data_event, reply_event in zip(data['events'], reply['events']): if reply_event['eventStatus'] != 200: - print 'Inara\t%s %s\t%s' % (reply_event['eventStatus'], reply_event.get('eventStatusText', ''), json.dumps(data_event, separators = (',', ': '))) + print 'Inara\t%s %s\t%s' % (reply_event['eventStatus'], reply_event.get('eventStatusText', ''), json.dumps(data_event)) if reply_event['eventStatus'] // 100 != 2: plug.show_error(_('Error: Inara {MSG}').format(MSG = '%s, %s' % (data_event['eventName'], reply_event.get('eventStatusText', reply_event['eventStatus'])))) if data_event['eventName'] in ['addCommanderTravelDock', 'addCommanderTravelFSDJump', 'setCommanderTravelLocation']: From 2aadc17a9d87ddadd0272ce09842050063078da2 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sat, 10 Feb 2018 19:19:37 +0000 Subject: [PATCH 09/15] Track ship loadout --- monitor.py | 76 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/monitor.py b/monitor.py index b60eca34..6fbc2115 100644 --- a/monitor.py +++ b/monitor.py @@ -113,7 +113,6 @@ class EDLogs(FileSystemEventHandler): 'Raw' : defaultdict(int), 'Manufactured' : defaultdict(int), 'Encoded' : defaultdict(int), - 'PaintJob' : None, 'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None }, 'Role' : None, # Crew role - None, Idle, FireCon, FighterCon 'Friends' : set(), # Online friends @@ -121,6 +120,10 @@ class EDLogs(FileSystemEventHandler): 'ShipIdent' : None, 'ShipName' : None, 'ShipType' : None, + 'HullValue' : None, + 'ModulesValue' : None, + 'Rebuy' : None, + 'Modules' : None, } def start(self, root): @@ -319,7 +322,6 @@ class EDLogs(FileSystemEventHandler): 'Raw' : defaultdict(int), 'Manufactured' : defaultdict(int), 'Encoded' : defaultdict(int), - 'PaintJob' : None, 'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None }, 'Role' : None, 'Friends' : set(), @@ -327,6 +329,10 @@ class EDLogs(FileSystemEventHandler): 'ShipIdent' : None, 'ShipName' : None, 'ShipType' : None, + 'HullValue' : None, + 'ModulesValue' : None, + 'Rebuy' : None, + 'Modules' : None, } elif entry['event'] == 'Commander': self.live = True # First event in 3.0 @@ -362,25 +368,45 @@ class EDLogs(FileSystemEventHandler): self.state['ShipIdent'] = None self.state['ShipName'] = None self.state['ShipType'] = self.canonicalise(entry['ShipType']) - self.state['PaintJob'] = None + self.state['HullValue'] = None + self.state['ModulesValue'] = None + self.state['Rebuy'] = None + self.state['Modules'] = None elif entry['event'] == 'ShipyardSwap': self.state['ShipID'] = entry['ShipID'] self.state['ShipIdent'] = None self.state['ShipName'] = None self.state['ShipType'] = self.canonicalise(entry['ShipType']) - self.state['PaintJob'] = None + self.state['HullValue'] = None + self.state['ModulesValue'] = None + self.state['Rebuy'] = None + self.state['Modules'] = None elif entry['event'] == 'Loadout': # Note: Precedes LoadGame, ShipyardNew, follows ShipyardSwap, ShipyardBuy self.state['ShipID'] = entry['ShipID'] self.state['ShipIdent'] = entry['ShipIdent'] self.state['ShipName'] = entry['ShipName'] self.state['ShipType'] = self.canonicalise(entry['Ship']) - # Ignore other Modules since they're missing Engineer modification details - self.state['PaintJob'] = 'paintjob_%s_default_defaultpaintjob' % self.state['ShipType'] - for module in entry['Modules']: - if module.get('Slot') == 'PaintJob' and module.get('Item'): - self.state['PaintJob'] = self.canonicalise(module['Item']) - elif entry['event'] in ['ModuleBuy', 'ModuleSell'] and entry['Slot'] == 'PaintJob': - self.state['PaintJob'] = self.canonicalise(entry.get('BuyItem')) + self.state['HullValue'] = entry.get('HullValue') # not present on exiting Outfitting + self.state['ModulesValue'] = entry.get('ModulesValue') # " + self.state['Rebuy'] = entry.get('Rebuy') + self.state['Modules'] = dict([(thing['Slot'], thing) for thing in entry['Modules']]) + elif entry['event'] == 'ModuleBuy': + self.state['Modules'][entry['Slot']] = { 'Slot' : entry['Slot'], + 'Item' : self.canonicalise(entry['BuyItem']), + 'On' : True, + 'Priority' : 1, + 'Health' : 1.0, + 'Value' : entry['BuyPrice'], + } + elif entry['event'] == 'ModuleSell': + self.state['Modules'].pop(entry['Slot'], None) + elif entry['event'] == 'ModuleSwap': + toitem = self.state['Modules'].get(entry['ToSlot']) + self.state['Modules'][entry['ToSlot']] = self.state['Modules'][entry['FromSlot']] + if toitem: + self.state['Modules'][entry['FromSlot']] = toitem + else: + self.state['Modules'].pop(entry['FromSlot'], None) elif entry['event'] in ['Undocked']: self.station = None self.stationtype = None @@ -447,15 +473,39 @@ class EDLogs(FileSystemEventHandler): self.state[entry['Category']][material] -= entry['Count'] if self.state[entry['Category']][material] <= 0: self.state[entry['Category']].pop(material) - elif entry['event'] in ['EngineerCraft', 'Synthesis']: + elif entry['event'] == 'Synthesis': for category in ['Raw', 'Manufactured', 'Encoded']: - for x in entry[entry['event'] == 'EngineerCraft' and 'Ingredients' or 'Materials']: + for x in entry['Materials']: material = self.canonicalise(x['Name']) if material in self.state[category]: self.state[category][material] -= x['Count'] if self.state[category][material] <= 0: self.state[category].pop(material) + elif entry['event'] == 'EngineerCraft' or (entry['event'] == 'EngineerLegacyConvert' and not entry.get('IsPreview')): + for category in ['Raw', 'Manufactured', 'Encoded']: + for x in entry.get('Ingredients', []): + material = self.canonicalise(x['Name']) + if material in self.state[category]: + self.state[category][material] -= x['Count'] + if self.state[category][material] <= 0: + self.state[category].pop(material) + module = self.state['Modules'][entry['Slot']] + module['Engineering'] = { + 'Engineer' : entry['Engineer'], + 'EngineerID' : entry['EngineerID'], + 'BlueprintName' : entry['BlueprintName'], + 'BlueprintID' : entry['BlueprintID'], + 'Level' : entry['Level'], + 'Quality' : entry['Quality'], + 'Modifiers' : entry['Modifiers'], + } + if 'ExperimentalEffect' in entry: + module['Engineering']['ExperimentalEffect'] = entry['ExperimentalEffect'] + module['Engineering']['ExperimentalEffect_Localised'] = entry['ExperimentalEffect_Localised'] + else: + module['Engineering'].pop('ExperimentalEffect', None) + module['Engineering'].pop('ExperimentalEffect_Localised', None) elif entry['event'] == 'EngineerContribution': commodity = self.canonicalise(entry.get('Commodity')) if commodity: From 88f323d36e1757cbd20d0ca6b877fac01f021ace Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sat, 10 Feb 2018 19:21:55 +0000 Subject: [PATCH 10/15] Switch EDShipyard import to Loadout event --- EDMarketConnector.py | 16 +++++++++------- edshipyard.py | 7 +++++-- monitor.py | 19 +++++++++++++++++++ plugins/edsm.py | 13 +++++++++---- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index aed8b007..b3498fb8 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -709,6 +709,14 @@ class AppWindow: if not monitor.cmdr or not monitor.mode: return False # In CQC - do nothing + if config.getint('shipyard') == config.SHIPYARD_EDSHIPYARD: + return edshipyard.url(monitor.is_beta) + elif config.getint('shipyard') == config.SHIPYARD_CORIOLIS: + pass # Fall through + else: + assert False, config.getint('shipyard') + return False + self.status['text'] = _('Fetching data...') self.w.update_idletasks() try: @@ -734,13 +742,7 @@ class AppWindow: self.status['text'] = _('Error: Frontier server is lagging') # Raised when Companion API server is returning old data, e.g. when the servers are too busy else: self.status['text'] = '' - if config.getint('shipyard') == config.SHIPYARD_EDSHIPYARD: - return edshipyard.url(data, monitor.is_beta) - elif config.getint('shipyard') == config.SHIPYARD_CORIOLIS: - return coriolis.url(data, monitor.is_beta) - else: - assert False, config.getint('shipyard') - return False + return coriolis.url(data, monitor.is_beta) def cooldown(self): if time() < self.holdofftime: diff --git a/edshipyard.py b/edshipyard.py index 8274ab51..31a1b5b9 100644 --- a/edshipyard.py +++ b/edshipyard.py @@ -13,6 +13,7 @@ import gzip from config import config import companion import outfitting +from monitor import monitor # Map API ship names to E:D Shipyard ship names ship_map = dict(companion.ship_map) @@ -162,9 +163,11 @@ def export(data, filename=None): # Return a URL for the current ship -def url(data, is_beta): +def url(is_beta): - string = json.dumps(companion.ship(data), ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') # most compact representation + string = json.dumps(monitor.ship(), ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8') # most compact representation + if not string: + return False out = StringIO.StringIO() with gzip.GzipFile(fileobj=out, mode='w') as f: diff --git a/monitor.py b/monitor.py index 6fbc2115..179d07cc 100644 --- a/monitor.py +++ b/monitor.py @@ -625,5 +625,24 @@ class EDLogs(FileSystemEventHandler): return False + # Return a subset of the received data describing the current ship as a Loadout event + def ship(self): + if not self.state['Modules']: + return None + + d = OrderedDict([ + ('timestamp', strftime('%Y-%m-%dT%H:%M:%SZ', gmtime())), + ('event', 'Loadout'), + ('Ship', self.state['ShipType']), + ('ShipID', self.state['ShipID']), + ]) + for thing in ['ShipName', 'ShipIdent', 'HullValue', 'ModulesValue', 'Rebuy']: + if self.state[thing]: + d[thing] = self.state[thing] + d['Modules'] = self.state['Modules'].values() + + return d + + # singleton monitor = EDLogs() diff --git a/plugins/edsm.py b/plugins/edsm.py index 5cb0b824..05cfb004 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -34,6 +34,7 @@ this.session = requests.Session() this.queue = Queue() # Items to be sent to EDSM by worker thread this.discardedEvents = [] # List discarded events from EDSM this.lastship = None # Description of last ship that we sent to EDSM +this.lastloadout = None # Description of last ship that we sent to EDSM this.lastlookup = False # whether the last lookup succeeded # Game state @@ -235,6 +236,14 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): this.queue.put((cmdr, entry)) + if entry['event'] == 'Loadout' and 'EDShipyard' not in this.discardedEvents: + url = edshipyard.url(is_beta) + if this.lastloadout != url: + this.lastloadout = url + this.queue.put((cmdr, { + 'event': 'EDShipyard', 'timestamp': entry['timestamp'], '_shipId': state['ShipID'], 'url': this.lastloadout + })) + # Update system data def cmdr_data(data, is_beta): @@ -262,10 +271,6 @@ def cmdr_data(data, is_beta): this.queue.put((cmdr, { 'event': 'Coriolis', 'timestamp': timestamp, '_shipId': data['ship']['id'], 'url': coriolis.url(data, is_beta) })) - if 'EDShipyard' not in this.discardedEvents: - this.queue.put((cmdr, { - 'event': 'EDShipyard', 'timestamp': timestamp, '_shipId': data['ship']['id'], 'url': edshipyard.url(data, is_beta) - })) this.lastship = ship From 2552c46d090bb9c3c92cb20104c835c1a4cb2b69 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sun, 18 Feb 2018 02:35:37 +0000 Subject: [PATCH 11/15] Track Reputation and Statistics --- monitor.py | 28 +++++++++++++++++++++------- plugins/inara.py | 9 +++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/monitor.py b/monitor.py index 179d07cc..de435069 100644 --- a/monitor.py +++ b/monitor.py @@ -113,7 +113,9 @@ class EDLogs(FileSystemEventHandler): 'Raw' : defaultdict(int), 'Manufactured' : defaultdict(int), 'Encoded' : defaultdict(int), - 'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None }, + 'Rank' : {}, + 'Reputation' : {}, + 'Statistics' : {}, 'Role' : None, # Crew role - None, Idle, FireCon, FighterCon 'Friends' : set(), # Online friends 'ShipID' : None, @@ -322,7 +324,9 @@ class EDLogs(FileSystemEventHandler): 'Raw' : defaultdict(int), 'Manufactured' : defaultdict(int), 'Encoded' : defaultdict(int), - 'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None }, + 'Rank' : {}, + 'Reputation' : {}, + 'Statistics' : {}, 'Role' : None, 'Friends' : set(), 'ShipID' : None, @@ -351,7 +355,9 @@ class EDLogs(FileSystemEventHandler): 'Captain' : None, 'Credits' : entry['Credits'], 'Loan' : entry['Loan'], - 'Rank' : { 'Combat': None, 'Trade': None, 'Explore': None, 'Empire': None, 'Federation': None, 'CQC': None }, + 'Rank' : {}, + 'Reputation' : {}, + 'Statistics' : {}, 'Role' : None, }) elif entry['event'] == 'NewCommander': @@ -430,14 +436,22 @@ class EDLogs(FileSystemEventHandler): self.planet = entry.get('Body') if entry.get('BodyType') == 'Planet' else None elif entry['event'] in ['LeaveBody', 'SupercruiseEntry']: self.planet = None + elif entry['event'] in ['Rank', 'Promotion']: - for k,v in entry.iteritems(): - if k in self.state['Rank']: - self.state['Rank'][k] = (v,0) + payload = dict(entry) + payload.pop('event') + payload.pop('timestamp') + for k,v in payload.iteritems(): + self.state['Rank'][k] = (v,0) elif entry['event'] == 'Progress': for k,v in entry.iteritems(): - if self.state['Rank'].get(k) is not None: + if k in self.state['Rank']: self.state['Rank'][k] = (self.state['Rank'][k][0], min(v, 100)) # perhaps not taken promotion mission yet + elif entry['event'] in ['Reputation', 'Statistics']: + payload = dict(entry) + payload.pop('event') + payload.pop('timestamp') + self.state[entry['event']] = payload elif entry['event'] == 'Cargo': self.state['Cargo'] = defaultdict(int) diff --git a/plugins/inara.py b/plugins/inara.py index e5392f6b..8873254e 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -181,6 +181,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): ('rankValue', v[0]), ('rankProgress', v[1] / 100.0), ])) + for k,v in state['Reputation'].iteritems(): + if v is not None: + add_event('setCommanderReputationMajorFaction', entry['timestamp'], + OrderedDict([ + ('majorfactionName', k.lower()), + ('majorfactionReputation', v / 100.0), + ])) + add_event('setCommanderGameStatistics', entry['timestamp'], state['Statistics']) # may be out of date + elif entry['event'] == 'Promotion': for k,v in state['Rank'].iteritems(): if k in entry: From 1de3edc4525948e1703ed02b458384a10c9f3fe0 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sun, 18 Feb 2018 02:36:32 +0000 Subject: [PATCH 12/15] MissionCompleted has MaterialsReward property --- monitor.py | 5 +++++ plugins/inara.py | 8 ++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/monitor.py b/monitor.py index de435069..05fd338b 100644 --- a/monitor.py +++ b/monitor.py @@ -468,6 +468,11 @@ class EDLogs(FileSystemEventHandler): for reward in entry.get('CommodityReward', []): commodity = self.canonicalise(reward['Name']) self.state['Cargo'][commodity] += reward.get('Count', 1) + for reward in entry.get('MaterialsReward', []): + if 'Category' in reward: # FIXME: Category not present in E:D 3.0 + material = self.canonicalise(reward['Name']) + self.state[reward['Category']][material] += reward.get('Count', 1) + elif entry['event'] == 'SearchAndRescue': for item in entry.get('Items', []): commodity = self.canonicalise(item['Name']) diff --git a/plugins/inara.py b/plugins/inara.py index 8873254e..e1eda1f7 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -419,14 +419,10 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): data['rewardPermits'] = [{ 'starsystemName': x } for x in entry['PermitsAwarded']] if 'CommodityReward' in entry: data['rewardCommodities'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['CommodityReward']] + if 'MaterialsReward' in entry: + data['rewardMaterials'] = [{ 'itemName': x['Name'], 'itemCount': x['Count'] } for x in entry['MaterialsReward']] add_event('setCommanderMissionCompleted', entry['timestamp'], data) - # Journal doesn't list rewarded materials directly, just as 'MaterialCollected' - elif (entry['event'] == 'MaterialCollected' and this.events and - this.events[-1]['eventName'] == 'setCommanderMissionCompleted' and - this.events[-1]['eventTimestamp'] == entry['timestamp']): - this.events[-1]['eventData']['rewardMaterials'] = [{ 'itemName': entry['Name'], 'itemCount': entry['Count'] }] - elif entry['event'] == 'MissionFailed': add_event('setCommanderMissionFailed', entry['timestamp'], { 'missionGameID': entry['MissionID'] }) From 4aa80c8426ee0da2a024f8cee8047754e7f9e709 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sun, 18 Feb 2018 02:41:10 +0000 Subject: [PATCH 13/15] Get fleet and loadout from Journal --- plugins/inara.py | 167 +++++++++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 57 deletions(-) diff --git a/plugins/inara.py b/plugins/inara.py index e1eda1f7..03a47d30 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -7,6 +7,7 @@ import json import requests import sys import time +from operator import itemgetter from Queue import Queue from threading import Thread @@ -40,7 +41,9 @@ this.suppress_docked = False # Skip initial Docked event if started docked this.cargo = None this.materials = None this.lastcredits = 0 # Send credit update soon after Startup / new game -this.needfleet = True # Send full fleet update soon after Startup / new game +this.storedmodules = None +this.loadout = None +this.fleet = None this.shipswap = False # just swapped ship # URLs @@ -154,7 +157,9 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): this.cargo = None this.materials = None this.lastcredits = 0 - this.needfleet = True + this.storedmodules = None + this.loadout = None + this.fleet = None this.shipswap = False elif entry['event'] in ['Resurrect', 'ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy']: # Events that mean a significant change in credits so we should send credits after next "Update" @@ -239,20 +244,26 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): if (entry['event'] in ['StartUp', 'Cargo'] or (entry['event'] == 'Loadout' and this.shipswap) or this.newuser): - if entry['event'] == 'ShipyardNew': - add_event('addCommanderShip', entry['timestamp'], - OrderedDict([ - ('shipType', state['ShipType']), - ('shipGameID', state['ShipID']), - ])) - add_event('setCommanderShip', entry['timestamp'], - OrderedDict([ - ('shipType', state['ShipType']), - ('shipGameID', state['ShipID']), - ('shipName', state['ShipName']), # Can be None - ('shipIdent', state['ShipIdent']), # Can be None - ('isCurrentShip', True), - ])) + data = OrderedDict([ + ('shipType', state['ShipType']), + ('shipGameID', state['ShipID']), + ('shipName', state['ShipName']), # Can be None + ('shipIdent', state['ShipIdent']), # Can be None + ('isCurrentShip', True), + ]) + if state['HullValue']: + data['shipHullValue'] = state['HullValue'] + if state['ModulesValue']: + data['shipModulesValue'] = state['ModulesValue'] + data['shipRebuyCost'] = state['Rebuy'] + add_event('setCommanderShip', entry['timestamp'], data) + + this.loadout = OrderedDict([ + ('shipType', state['ShipType']), + ('shipGameID', state['ShipID']), + ('shipLoadout', state['Modules'].values()), + ]) + add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) this.shipswap = False # Update location @@ -334,7 +345,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): # # Selling / swapping ships - if entry['event'] in ['ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap']: + if entry['event'] == 'ShipyardNew': + add_event('addCommanderShip', entry['timestamp'], + OrderedDict([ + ('shipType', state['ShipType']), + ('shipGameID', state['ShipID']), + ])) + this.shipswap = True # Want subsequent Loadout event to be sent immediately + + elif entry['event'] in ['ShipyardBuy', 'ShipyardSell', 'SellShipOnRebuy', 'ShipyardSwap']: if entry['event'] == 'ShipyardSwap': this.shipswap = True # Don't know new ship name and ident 'til the following Loadout event if 'StoreShipID' in entry: @@ -372,6 +391,79 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): ('transferTime', entry['TransferTime']), ])) + # Fleet + if entry['event'] == 'StoredShips': + fleet = sorted( + [{ + 'shipType': x['ShipType'], + 'shipGameID': x['ShipID'], + 'shipName': x.get('Name'), + 'isHot': x['Hot'], + 'starsystemName': entry['StarSystem'], + 'stationName': entry['StationName'], + 'marketID': entry['MarketID'], + } for x in entry['ShipsHere']] + + [{ + 'shipType': x['ShipType'], + 'shipGameID': x['ShipID'], + 'shipName': x.get('Name'), + 'isHot': x['Hot'], + 'starsystemName': x.get('StarSystem'), # Not present for ships in transit + 'marketID': x.get('ShipMarketID'), # " + } for x in entry['ShipsRemote']], + key = itemgetter('shipGameID') + ) + if this.fleet != fleet: + this.fleet = fleet + this.events = [x for x in this.events if x['eventName'] != 'setCommanderShip'] # Remove any unsent + for ship in this.fleet: + add_event('setCommanderShip', entry['timestamp'], ship) + + # Loadout + if entry['event'] == 'Loadout': + loadout = OrderedDict([ + ('shipType', state['ShipType']), + ('shipGameID', state['ShipID']), + ('shipLoadout', state['Modules'].values()), + ]) + if this.loadout != loadout: + this.loadout = loadout + this.events = [x for x in this.events if x['eventName'] != 'setCommanderShipLoadout' or x['shipGameID'] != this.loadout['shipGameID']] # Remove any unsent for this ship + add_event('setCommanderShipLoadout', entry['timestamp'], this.loadout) + + # Stored modules + if entry['event'] == 'StoredModules': + items = dict([(x['StorageSlot'], x) for x in entry['Items']]) # Impose an order + modules = [] + for slot in sorted(items): + item = items[slot] + module = OrderedDict([ + ('itemName', item['Name']), + ('itemValue', item['BuyPrice']), + ('isHot', item['Hot']), + ]) + + # Location can be absent if in transit + if 'StarSystem' in item: + module['starsystemName'] = item['StarSystem'] + if 'MarketID' in item: + module['marketID'] = item['MarketID'] + + if 'EngineerModifications' in item: + module['engineering'] = OrderedDict([('blueprintName', item['EngineerModifications'])]) + if 'Level' in item: + module['engineering']['blueprintLevel'] = item['Level'] + if 'Quality' in item: + module['engineering']['blueprintQuality'] = item['Quality'] + + modules.append(module) + + if this.storedmodules != modules: + # Only send on change + this.storedmodules = modules + this.events = [x for x in this.events if x['eventName'] != 'setCommanderStorageModules'] # Remove any unsent + add_event('setCommanderStorageModules', entry['timestamp'], this.storedmodules) + # Missions if entry['event'] == 'MissionAccepted': data = OrderedDict([ @@ -508,54 +600,15 @@ def cmdr_data(data, is_beta): this.cmdr = data['commander']['name'] if config.getint('inara_out') and not is_beta and not this.multicrew and credentials(this.cmdr): - - timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) - assets = data['commander']['credits'] - data['commander'].get('debt', 0) - - for ship in companion.listify(data.get('ships', [])): - if ship: - assets += ship['value']['total'] - if this.needfleet: - if ship['id'] != data['commander']['currentShipId']: - add_event('setCommanderShip', timestamp, - OrderedDict([ - ('shipType', ship['name']), - ('shipGameID', ship['id']), - ('shipName', ship.get('shipName')), # Can be None - ('shipIdent', ship.get('shipID')), # Can be None - ('shipHullValue', ship['value']['hull']), - ('shipModulesValue', ship['value']['modules']), - ('starsystemName', ship['starsystem']['name']), - ('stationName', ship['station']['name']), - ])) - else: - add_event('setCommanderShip', timestamp, - OrderedDict([ - ('shipType', ship['name']), - ('shipGameID', ship['id']), - ('shipName', ship.get('shipName')), # Can be None - ('shipIdent', ship.get('shipID')), # Can be None - ('isCurrentShip', True), - ('shipHullValue', ship['value']['hull']), - ('shipModulesValue', ship['value']['modules']), - ])) - if not (CREDIT_RATIO > this.lastcredits / data['commander']['credits'] > 1/CREDIT_RATIO): this.events = [x for x in this.events if x['eventName'] != 'setCommanderCredits'] # Remove any unsent - add_event('setCommanderCredits', timestamp, + add_event('setCommanderCredits', time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), OrderedDict([ ('commanderCredits', data['commander']['credits']), - ('commanderAssets', assets), ('commanderLoan', data['commander'].get('debt', 0)), ])) this.lastcredits = float(data['commander']['credits']) - # *Don't* queue a call to Inara if we're just updating credits - wait for next mandatory event - if this.needfleet: - call() - this.needfleet = False - - def add_event(name, timestamp, data): this.events.append(OrderedDict([ ('eventName', name), From 60ddc55d5e609d854f897a4605590b08a3660b2d Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sun, 18 Feb 2018 02:41:56 +0000 Subject: [PATCH 14/15] Send CommunityGoal TopTier info --- plugins/inara.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/inara.py b/plugins/inara.py index 03a47d30..25293337 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -580,6 +580,9 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): data['tierReached'] = int(goal['TierReached'].split()[-1]) if 'TopRankSize' in goal: data['topRankSize'] = goal['TopRankSize'] + if 'TopTier' in goal: + data['tierMax'] = int(goal['TopTier']['Name'].split()[-1]) + data['completionBonus'] = goal['TopTier']['Bonus'] add_event('setCommunityGoal', entry['timestamp'], data) data = OrderedDict([ From 3a001666214a0a41a47a9e0fb4cad9daa5b801c4 Mon Sep 17 00:00:00 2001 From: Jonathan Harris <jonathan@marginal.org.uk> Date: Sun, 18 Feb 2018 02:56:23 +0000 Subject: [PATCH 15/15] Send credits at startup --- plugins/inara.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/inara.py b/plugins/inara.py index 25293337..3ee5d031 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -176,6 +176,15 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): try: old_events = len(this.events) # Will only send existing events if we add a new event below + # Send credits to Inara on startup only - otherwise may be out of date + if entry['event'] == 'Cargo': + add_event('setCommanderCredits', entry['timestamp'], + OrderedDict([ + ('commanderCredits', state['Credits']), + ('commanderLoan', state['Loan']), + ])) + this.lastcredits = state['Credits'] + # Send rank info to Inara on startup or change if (entry['event'] in ['StartUp', 'Cargo'] or this.newuser): for k,v in state['Rank'].iteritems():