1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
""" |
3
|
|
|
This module provides a set of parsers which can be used to obtain information |
4
|
|
|
about IL-2 FB missions. Every parser is a one-pass parser. They can be used to |
5
|
|
|
parse a whole mission file or to parse a single given section as well. |
6
|
|
|
""" |
7
|
|
|
|
8
|
1 |
|
import datetime |
9
|
1 |
|
import math |
10
|
1 |
|
import six |
11
|
1 |
|
import sys |
12
|
|
|
|
13
|
1 |
|
from abc import ABCMeta, abstractmethod |
14
|
|
|
|
15
|
1 |
|
from il2fb.commons.flight import Formations, RoutePointTypes |
16
|
1 |
|
from il2fb.commons.organization import AirForces, Belligerents, Regiments |
17
|
1 |
|
from il2fb.commons.spatial import Point2D, Point3D |
18
|
1 |
|
from il2fb.commons.targets import TargetTypes, TargetPriorities |
19
|
1 |
|
from il2fb.commons.weather import Conditions, Gust, Turbulence |
20
|
|
|
|
21
|
1 |
|
from .constants import ( |
22
|
|
|
IS_STATIONARY_AIRCRAFT_RESTORABLE, NULL, WEAPONS_CONTINUATION_MARK, |
23
|
|
|
ROUTE_POINT_EXTRA_PARAMETERS_MARK, ROUTE_POINT_RADIO_SILENCE_ON, |
24
|
|
|
ROUTE_POINT_RADIO_SILENCE_OFF, |
25
|
|
|
) |
26
|
1 |
|
from .converters import ( |
27
|
|
|
to_bool, to_belligerent, to_skill, to_unit_type, to_air_force, |
28
|
|
|
) |
29
|
1 |
|
from .exceptions import MissionParsingError |
30
|
1 |
|
from .utils import move_if_present, set_if_present, strip_comments |
31
|
1 |
|
from .structures import ( |
32
|
|
|
GroundRoutePoint, Building, StaticCamera, FrontMarker, Rocket, |
33
|
|
|
StationaryObject, StationaryArtillery, StationaryAircraft, StationaryShip, |
34
|
|
|
FlightRoutePoint, FlightRouteTakeoffPoint, FlightRoutePatrolPoint, |
35
|
|
|
FlightRouteAttackPoint, |
36
|
|
|
) |
37
|
|
|
|
38
|
|
|
|
39
|
1 |
|
class SectionParser(object): |
40
|
|
|
""" |
41
|
|
|
Abstract base parser of a single section in a mission file. |
42
|
|
|
|
43
|
|
|
A common approach to parse a section can be described in the following way: |
44
|
|
|
|
45
|
|
|
#. Pass a section name (e.g. 'MAIN') to :meth:`start` method. If parser can |
46
|
|
|
process a section with such name, it will return `True` and then you can |
47
|
|
|
proceed. |
48
|
|
|
#. Pass section lines one-by-one to :meth:`parse_line`. |
49
|
|
|
#. When you are done, get your parsed data by calling :meth:`stop`. This |
50
|
|
|
will tell the parser that no more data will be given and the parsing can |
51
|
|
|
be finished. |
52
|
|
|
|
53
|
|
|
| |
54
|
|
|
**Example**: |
55
|
|
|
|
56
|
|
|
.. code-block:: python |
57
|
|
|
|
58
|
|
|
section_name = "Test section" |
59
|
|
|
lines = ["foo", "bar", "baz", "qux", ] |
60
|
|
|
parser = SomeParser() |
61
|
|
|
|
62
|
|
|
if parser.start(section_name): |
63
|
|
|
for line in lines: |
64
|
|
|
parser.parse_line(line) |
65
|
|
|
result = parser.stop() |
66
|
|
|
|
67
|
|
|
""" |
68
|
|
|
|
69
|
1 |
|
__metaclass__ = ABCMeta |
70
|
|
|
|
71
|
|
|
#: Tells whether a parser was started. |
72
|
1 |
|
running = False |
73
|
|
|
|
74
|
|
|
#: An internal buffer which can be redefined. |
75
|
1 |
|
data = None |
76
|
|
|
|
77
|
1 |
|
def start(self, section_name): |
78
|
|
|
""" |
79
|
|
|
Try to start a parser. If a section with given name can be parsed, the |
80
|
|
|
parser will initialize it's internal data structures and set |
81
|
|
|
:attr:`running` to `True`. |
82
|
|
|
|
83
|
|
|
:param str section_name: a name of section which is going to be parsed |
84
|
|
|
|
85
|
|
|
:returns: `True` if section with a given name can be parsed by parser, |
86
|
|
|
`False` otherwise |
87
|
|
|
:rtype: :class:`bool` |
88
|
|
|
""" |
89
|
1 |
|
result = self.check_section_name(section_name) |
90
|
1 |
|
if result: |
91
|
1 |
|
self.running = True |
92
|
1 |
|
self.init_parser(section_name) |
93
|
1 |
|
return result |
94
|
|
|
|
95
|
1 |
|
@abstractmethod |
96
|
|
|
def check_section_name(self, section_name): |
97
|
|
|
""" |
98
|
|
|
Check whether a section with a given name can be parsed. |
99
|
|
|
|
100
|
|
|
:param str section_name: a name of section which is going to be parsed |
101
|
|
|
|
102
|
|
|
:returns: `True` if section with a given name can be parsed by parser, |
103
|
|
|
`False` otherwise |
104
|
|
|
:rtype: :class:`bool` |
105
|
|
|
""" |
106
|
|
|
|
107
|
1 |
|
@abstractmethod |
108
|
|
|
def init_parser(self, section_name): |
109
|
|
|
""" |
110
|
|
|
Abstract method which is called by :meth:`start` to initialize |
111
|
|
|
internal data structures. |
112
|
|
|
|
113
|
|
|
:param str section_name: a name of section which is going to be parsed |
114
|
|
|
|
115
|
|
|
:returns: ``None`` |
116
|
|
|
""" |
117
|
|
|
|
118
|
1 |
|
@abstractmethod |
119
|
|
|
def parse_line(self, line): |
120
|
|
|
""" |
121
|
|
|
Abstract method which is called manually to parse a line from mission |
122
|
|
|
section. |
123
|
|
|
|
124
|
|
|
:param str line: a single line to parse |
125
|
|
|
|
126
|
|
|
:returns: ``None`` |
127
|
|
|
""" |
128
|
|
|
|
129
|
1 |
|
def stop(self): |
130
|
|
|
""" |
131
|
|
|
Stops parser and returns fully processed data. |
132
|
|
|
|
133
|
|
|
:returns: a data structure returned by :meth:`clean` method |
134
|
|
|
|
135
|
|
|
:raises RuntimeError: if parser was not started |
136
|
|
|
""" |
137
|
1 |
|
if not self.running: |
138
|
1 |
|
raise RuntimeError("Cannot stop parser which is not running") |
139
|
|
|
|
140
|
1 |
|
self.running = False |
141
|
1 |
|
return self.clean() |
142
|
|
|
|
143
|
1 |
|
def clean(self): |
144
|
|
|
""" |
145
|
|
|
Returns fully parsed data. Is called by :meth:`stop` method. |
146
|
|
|
|
147
|
|
|
:returns: a data structure which is specific for every subclass |
148
|
|
|
""" |
149
|
1 |
|
return self.data |
150
|
|
|
|
151
|
|
|
|
152
|
1 |
|
class ValuesParser(SectionParser): |
153
|
|
|
""" |
154
|
|
|
This is a base class for parsers which assume that a section, which is |
155
|
|
|
going to be parsed, consists of key-value pairs with unique keys, one pair |
156
|
|
|
per line. |
157
|
|
|
|
158
|
|
|
**Section definition example**:: |
159
|
|
|
|
160
|
|
|
[some section name] |
161
|
|
|
key1 value1 |
162
|
|
|
key2 value2 |
163
|
|
|
key3 value3 |
164
|
|
|
""" |
165
|
|
|
|
166
|
1 |
|
def init_parser(self, section_name): |
167
|
|
|
""" |
168
|
|
|
Implements abstract method. See :meth:`SectionParser.init_parser` for |
169
|
|
|
semantics. |
170
|
|
|
|
171
|
|
|
Initializes a dictionary to store raw keys and their values. |
172
|
|
|
""" |
173
|
1 |
|
self.data = {} |
174
|
|
|
|
175
|
1 |
|
def parse_line(self, line): |
176
|
|
|
""" |
177
|
|
|
Implements abstract method. See :meth:`SectionParser.parse_line` for |
178
|
|
|
semantics. |
179
|
|
|
|
180
|
|
|
Splits line into key-value pair and puts it into internal dictionary. |
181
|
|
|
""" |
182
|
1 |
|
key, value = line.strip().split() |
183
|
1 |
|
self.data.update({key: value}) |
184
|
|
|
|
185
|
|
|
|
186
|
1 |
|
class CollectingParser(SectionParser): |
187
|
|
|
""" |
188
|
|
|
This is a base class for parsers which assume that a section, which is |
189
|
|
|
going to be parsed, consists of homogeneous lines which describe different |
190
|
|
|
objects with one set of attributes. |
191
|
|
|
|
192
|
|
|
**Section definition example**:: |
193
|
|
|
|
194
|
|
|
[some section name] |
195
|
|
|
object1_attr1 object1_attr2 object1_attr3 object1_attr4 |
196
|
|
|
object2_attr1 object2_attr2 object2_attr3 object2_attr4 |
197
|
|
|
object3_attr1 object3_attr2 object3_attr3 object3_attr4 |
198
|
|
|
""" |
199
|
|
|
|
200
|
1 |
|
def init_parser(self, section_name): |
201
|
|
|
""" |
202
|
|
|
Implements abstract method. See :meth:`SectionParser.init_parser` for |
203
|
|
|
semantics. |
204
|
|
|
|
205
|
|
|
Initializes a list for storing collection of objects. |
206
|
|
|
""" |
207
|
1 |
|
self.data = [] |
208
|
|
|
|
209
|
1 |
|
def parse_line(self, line): |
210
|
|
|
""" |
211
|
|
|
Implements abstract method. See :meth:`SectionParser.parse_line` for |
212
|
|
|
semantics. |
213
|
|
|
|
214
|
|
|
Just puts entire line to internal buffer. You probably will want to |
215
|
|
|
redefine this method to do some extra job on each line. |
216
|
|
|
""" |
217
|
1 |
|
self.data.append(line.strip()) |
218
|
|
|
|
219
|
|
|
|
220
|
1 |
|
class MainParser(ValuesParser): |
221
|
|
|
""" |
222
|
|
|
Parses ``MAIN`` section. |
223
|
|
|
View :ref:`detailed description <main-section>`. |
224
|
|
|
""" |
225
|
|
|
|
226
|
1 |
|
def check_section_name(self, section_name): |
227
|
|
|
""" |
228
|
|
|
Implements abstract method. See |
229
|
|
|
:meth:`SectionParser.check_section_name` for semantics. |
230
|
|
|
""" |
231
|
1 |
|
return section_name == "MAIN" |
232
|
|
|
|
233
|
1 |
|
def clean(self): |
234
|
|
|
""" |
235
|
|
|
Redefines base method. See :meth:`SectionParser.clean` for |
236
|
|
|
semantics. |
237
|
|
|
""" |
238
|
1 |
|
weather_conditions = int(self.data['CloudType']) |
239
|
1 |
|
return { |
240
|
|
|
'location_loader': self.data['MAP'], |
241
|
|
|
'time': { |
242
|
|
|
'value': self._to_time(self.data['TIME']), |
243
|
|
|
'is_fixed': 'TIMECONSTANT' in self.data, |
244
|
|
|
}, |
245
|
|
|
'weather_conditions': Conditions.get_by_value(weather_conditions), |
246
|
|
|
'cloud_base': int(float(self.data['CloudHeight'])), |
247
|
|
|
'player': { |
248
|
|
|
'belligerent': to_belligerent(self.data['army']), |
249
|
|
|
'flight_id': self.data.get('player'), |
250
|
|
|
'aircraft_index': int(self.data['playerNum']), |
251
|
|
|
'fixed_weapons': 'WEAPONSCONSTANT' in self.data, |
252
|
|
|
}, |
253
|
|
|
} |
254
|
|
|
|
255
|
1 |
|
def _to_time(self, value): |
256
|
1 |
|
time = float(value) |
257
|
1 |
|
minutes, hours = math.modf(time) |
258
|
1 |
|
return datetime.time(int(hours), int(minutes * 60)) |
259
|
|
|
|
260
|
|
|
|
261
|
1 |
|
class SeasonParser(ValuesParser): |
262
|
|
|
""" |
263
|
|
|
Parses ``SEASON`` section. |
264
|
|
|
View :ref:`detailed description <season-section>`. |
265
|
|
|
""" |
266
|
|
|
|
267
|
1 |
|
def check_section_name(self, section_name): |
268
|
|
|
""" |
269
|
|
|
Implements abstract method. See |
270
|
|
|
:meth:`SectionParser.check_section_name` for semantics. |
271
|
|
|
""" |
272
|
1 |
|
return section_name == "SEASON" |
273
|
|
|
|
274
|
1 |
|
def clean(self): |
275
|
|
|
""" |
276
|
|
|
Redefines base method. See :meth:`SectionParser.clean` for |
277
|
|
|
semantics. |
278
|
|
|
|
279
|
|
|
Combines day, time and year into :class:`datetime.date` object. |
280
|
|
|
""" |
281
|
1 |
|
date = datetime.date(int(self.data['Year']), |
282
|
|
|
int(self.data['Month']), |
283
|
|
|
int(self.data['Day'])) |
284
|
1 |
|
return {'date': date, } |
285
|
|
|
|
286
|
|
|
|
287
|
1 |
|
class WeatherParser(ValuesParser): |
288
|
|
|
""" |
289
|
|
|
Parses ``WEATHER`` section. |
290
|
|
|
View :ref:`detailed description <weather-section>`. |
291
|
|
|
""" |
292
|
|
|
|
293
|
1 |
|
def check_section_name(self, section_name): |
294
|
|
|
""" |
295
|
|
|
Implements abstract method. See |
296
|
|
|
:meth:`SectionParser.check_section_name` for semantics. |
297
|
|
|
""" |
298
|
1 |
|
return section_name == "WEATHER" |
299
|
|
|
|
300
|
1 |
|
def clean(self): |
301
|
|
|
""" |
302
|
|
|
Redefines base method. See :meth:`SectionParser.clean` for |
303
|
|
|
semantics. |
304
|
|
|
""" |
305
|
1 |
|
gust = int(self.data['Gust']) |
306
|
1 |
|
turbulence = int(self.data['Turbulence']) |
307
|
1 |
|
return { |
308
|
|
|
'weather': { |
309
|
|
|
'wind': { |
310
|
|
|
'direction': float(self.data['WindDirection']), |
311
|
|
|
'speed': float(self.data['WindSpeed']), |
312
|
|
|
}, |
313
|
|
|
'gust': Gust.get_by_value(gust), |
314
|
|
|
'turbulence': Turbulence.get_by_value(turbulence), |
315
|
|
|
}, |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
|
319
|
1 |
|
class MDSParser(ValuesParser): |
320
|
|
|
""" |
321
|
|
|
Parses ``MDS`` section. |
322
|
|
|
View :ref:`detailed description <mds-section>`. |
323
|
|
|
""" |
324
|
|
|
|
325
|
1 |
|
def check_section_name(self, section_name): |
326
|
1 |
|
return section_name == "MDS" |
327
|
|
|
|
328
|
1 |
|
def parse_line(self, line): |
329
|
1 |
|
super(MDSParser, self).parse_line(line.replace('MDS_', '')) |
330
|
|
|
|
331
|
1 |
|
def clean(self): |
332
|
1 |
|
return { |
333
|
|
|
'conditions': { |
334
|
|
|
'radar': { |
335
|
|
|
'advanced_mode': to_bool(self.data['Radar_SetRadarToAdvanceMode']), |
336
|
|
|
'refresh_interval': int(self.data['Radar_RefreshInterval']), |
337
|
|
|
'ships': { |
338
|
|
|
'big': { |
339
|
|
|
'max_range': int(self.data['Radar_ShipRadar_MaxRange']), |
340
|
|
|
'min_height': int(self.data['Radar_ShipRadar_MinHeight']), |
341
|
|
|
'max_height': int(self.data['Radar_ShipRadar_MaxHeight']), |
342
|
|
|
}, |
343
|
|
|
'small': { |
344
|
|
|
'max_range': int(self.data['Radar_ShipSmallRadar_MaxRange']), |
345
|
|
|
'min_height': int(self.data['Radar_ShipSmallRadar_MinHeight']), |
346
|
|
|
'max_height': int(self.data['Radar_ShipSmallRadar_MaxHeight']), |
347
|
|
|
}, |
348
|
|
|
}, |
349
|
|
|
'scouts': { |
350
|
|
|
'max_range': int(self.data['Radar_ScoutRadar_MaxRange']), |
351
|
|
|
'max_height': int(self.data['Radar_ScoutRadar_DeltaHeight']), |
352
|
|
|
'alpha': int(self.data['Radar_ScoutGroundObjects_Alpha']), |
353
|
|
|
}, |
354
|
|
|
}, |
355
|
|
|
'scouting': { |
356
|
|
|
'ships_affect_radar': to_bool(self.data['Radar_ShipsAsRadar']), |
357
|
|
|
'scouts_affect_radar': to_bool(self.data['Radar_ScoutsAsRadar']), |
358
|
|
|
'only_scouts_complete_targets': to_bool(self.data['Radar_ScoutCompleteRecon']), |
359
|
|
|
}, |
360
|
|
|
'communication': { |
361
|
|
|
'tower_communication': to_bool(self.data['Radar_EnableTowerCommunications']), |
362
|
|
|
'vectoring': not to_bool(self.data['Radar_DisableVectoring']), |
363
|
|
|
'ai_radio_silence': to_bool(self.data['Misc_DisableAIRadioChatter']), |
364
|
|
|
}, |
365
|
|
|
'home_bases': { |
366
|
|
|
'hide_ai_aircrafts_after_landing': to_bool(self.data['Misc_DespawnAIPlanesAfterLanding']), |
367
|
|
|
'hide_unpopulated': to_bool(self.data['Radar_HideUnpopulatedAirstripsFromMinimap']), |
368
|
|
|
'hide_players_count': to_bool(self.data['Misc_HidePlayersCountOnHomeBase']), |
369
|
|
|
}, |
370
|
|
|
'crater_visibility_muptipliers': { |
371
|
|
|
'le_100kg': float(self.data['Misc_BombsCat1_CratersVisibilityMultiplier']), |
372
|
|
|
'le_1000kg': float(self.data['Misc_BombsCat2_CratersVisibilityMultiplier']), |
373
|
|
|
'gt_1000kg': float(self.data['Misc_BombsCat3_CratersVisibilityMultiplier']), |
374
|
|
|
}, |
375
|
|
|
}, |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
|
379
|
1 |
|
class MDSScoutsParser(CollectingParser): |
380
|
|
|
""" |
381
|
|
|
Parses ``MDS_Scouts`` section. |
382
|
|
|
View :ref:`detailed description <mds-scouts-section>`. |
383
|
|
|
""" |
384
|
1 |
|
input_prefix = "MDS_Scouts_" |
385
|
1 |
|
output_prefix = "scouts_" |
386
|
|
|
|
387
|
1 |
|
def check_section_name(self, section_name): |
388
|
1 |
|
if not section_name.startswith(self.input_prefix): |
389
|
1 |
|
return False |
390
|
1 |
|
belligerent_name = self._get_belligerent_name(section_name) |
391
|
1 |
|
return bool(belligerent_name) |
392
|
|
|
|
393
|
1 |
|
def init_parser(self, section_name): |
394
|
1 |
|
super(MDSScoutsParser, self).init_parser(section_name) |
395
|
1 |
|
belligerent_name = self._get_belligerent_name(section_name) |
396
|
1 |
|
self.belligerent = Belligerents[belligerent_name] |
397
|
1 |
|
self.output_key = "{}{}".format(self.output_prefix, belligerent_name) |
398
|
|
|
|
399
|
1 |
|
def _get_belligerent_name(self, section_name): |
400
|
1 |
|
return section_name[len(self.input_prefix):].lower() |
401
|
|
|
|
402
|
1 |
|
def clean(self): |
403
|
1 |
|
return { |
404
|
|
|
self.output_key: { |
405
|
|
|
'belligerent': self.belligerent, |
406
|
|
|
'aircrafts': self.data, |
407
|
|
|
}, |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
|
411
|
1 |
|
class RespawnTimeParser(ValuesParser): |
412
|
|
|
""" |
413
|
|
|
Parses ``RespawnTime`` section. |
414
|
|
|
View :ref:`detailed description <respawn-time-section>`. |
415
|
|
|
""" |
416
|
|
|
|
417
|
1 |
|
def check_section_name(self, section_name): |
418
|
1 |
|
return section_name == "RespawnTime" |
419
|
|
|
|
420
|
1 |
|
def clean(self): |
421
|
1 |
|
return { |
422
|
|
|
'respawn_time': { |
423
|
|
|
'ships': { |
424
|
|
|
'big': int(self.data['Bigship']), |
425
|
|
|
'small': int(self.data['Ship']), |
426
|
|
|
}, |
427
|
|
|
'balloons': int(self.data['Aeroanchored']), |
428
|
|
|
'artillery': int(self.data['Artillery']), |
429
|
|
|
'searchlights': int(self.data['Searchlight']), |
430
|
|
|
}, |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
|
434
|
1 |
|
class ChiefsParser(CollectingParser): |
435
|
|
|
""" |
436
|
|
|
Parses ``Chiefs`` section. |
437
|
|
|
View :ref:`detailed description <chiefs-section>`. |
438
|
|
|
""" |
439
|
|
|
|
440
|
1 |
|
def check_section_name(self, section_name): |
441
|
1 |
|
return section_name == "Chiefs" |
442
|
|
|
|
443
|
1 |
|
def parse_line(self, line): |
444
|
1 |
|
params = line.split() |
445
|
1 |
|
(uid, type_code, belligerent), params = params[0:3], params[3:] |
446
|
|
|
|
447
|
1 |
|
chief_type, code = type_code.split('.') |
448
|
1 |
|
try: |
449
|
1 |
|
chief_type = to_unit_type(chief_type) |
450
|
1 |
|
except Exception: |
451
|
|
|
# Use original string as unit type |
452
|
|
|
pass |
453
|
|
|
|
454
|
1 |
|
unit = { |
455
|
|
|
'id': uid, |
456
|
|
|
'code': code, |
457
|
|
|
'type': chief_type, |
458
|
|
|
'belligerent': to_belligerent(belligerent), |
459
|
|
|
} |
460
|
1 |
|
if params: |
461
|
1 |
|
hibernation, skill, recharge_time = params |
462
|
1 |
|
unit.update({ |
463
|
|
|
'hibernation': int(hibernation), |
464
|
|
|
'skill': to_skill(skill), |
465
|
|
|
'recharge_time': float(recharge_time), |
466
|
|
|
}) |
467
|
1 |
|
self.data.append(unit) |
468
|
|
|
|
469
|
1 |
|
def clean(self): |
470
|
1 |
|
return {'moving_units': self.data, } |
471
|
|
|
|
472
|
|
|
|
473
|
1 |
|
class ChiefRoadParser(CollectingParser): |
474
|
|
|
""" |
475
|
|
|
Parses ``N_Chief_Road`` section. |
476
|
|
|
View :ref:`detailed description <chief-road-section>`. |
477
|
|
|
""" |
478
|
1 |
|
id_suffix = "_Chief" |
479
|
1 |
|
section_suffix = "_Road" |
480
|
1 |
|
input_suffix = id_suffix + section_suffix |
481
|
1 |
|
output_prefix = 'route_' |
482
|
|
|
|
483
|
1 |
|
def check_section_name(self, section_name): |
484
|
1 |
|
if not section_name.endswith(self.input_suffix): |
485
|
1 |
|
return False |
486
|
1 |
|
unit_id = self._extract_unit_id(section_name) |
487
|
1 |
|
stop = unit_id.index(self.id_suffix) |
488
|
1 |
|
return unit_id[:stop].isdigit() |
489
|
|
|
|
490
|
1 |
|
def init_parser(self, section_name): |
491
|
1 |
|
super(ChiefRoadParser, self).init_parser(section_name) |
492
|
1 |
|
unit_id = self._extract_unit_id(section_name) |
493
|
1 |
|
self.output_key = "{}{}".format(self.output_prefix, unit_id) |
494
|
|
|
|
495
|
1 |
|
def _extract_unit_id(self, section_name): |
496
|
1 |
|
stop = section_name.index(self.section_suffix) |
497
|
1 |
|
return section_name[:stop] |
498
|
|
|
|
499
|
1 |
|
def parse_line(self, line): |
500
|
1 |
|
params = line.split() |
501
|
1 |
|
pos, params = params[0:2], params[3:] |
502
|
|
|
|
503
|
1 |
|
args = { |
504
|
|
|
'pos': Point2D(*pos), |
505
|
|
|
} |
506
|
1 |
|
is_checkpoint = bool(params) |
507
|
1 |
|
args['is_checkpoint'] = is_checkpoint |
508
|
1 |
|
if is_checkpoint: |
509
|
1 |
|
args['delay'] = int(params[0]) |
510
|
1 |
|
args['section_length'] = int(params[1]) |
511
|
1 |
|
args['speed'] = float(params[2]) |
512
|
|
|
|
513
|
1 |
|
point = GroundRoutePoint(**args) |
514
|
1 |
|
self.data.append(point) |
515
|
|
|
|
516
|
1 |
|
def clean(self): |
517
|
1 |
|
return {self.output_key: self.data} |
518
|
|
|
|
519
|
|
|
|
520
|
1 |
|
class NStationaryParser(CollectingParser): |
521
|
|
|
""" |
522
|
|
|
Parses ``NStationary`` section. |
523
|
|
|
View :ref:`detailed description <nstationary-section>`. |
524
|
|
|
""" |
525
|
|
|
|
526
|
1 |
|
def __parse_artillery(params): |
527
|
|
|
""" |
528
|
|
|
Parse additional options for ``artillery`` type |
529
|
|
|
""" |
530
|
1 |
|
try: |
531
|
1 |
|
awakening_time, the_range, skill, use_spotter = params |
532
|
1 |
|
skill = to_skill(skill) |
533
|
1 |
|
use_spotter = to_bool(use_spotter) |
534
|
1 |
|
except ValueError: |
535
|
1 |
|
try: |
536
|
1 |
|
awakening_time, the_range = params |
537
|
1 |
|
except ValueError: |
538
|
1 |
|
awakening_time, the_range = params[0], 0 |
539
|
1 |
|
skill, use_spotter = None, False |
540
|
|
|
|
541
|
1 |
|
return { |
542
|
|
|
'awakening_time': float(awakening_time), |
543
|
|
|
'range': int(the_range), |
544
|
|
|
'skill': skill, |
545
|
|
|
'use_spotter': use_spotter, |
546
|
|
|
} |
547
|
|
|
|
548
|
1 |
|
def __parse_aircraft(params): |
549
|
|
|
""" |
550
|
|
|
Parse additional options for ``planes`` type |
551
|
|
|
""" |
552
|
1 |
|
try: |
553
|
1 |
|
air_force, allows_spawning__restorable = params[1:3] |
554
|
1 |
|
skin, show_markings = params[4:] |
555
|
1 |
|
except ValueError: |
556
|
1 |
|
air_force, allows_spawning__restorable = None, 0 |
557
|
1 |
|
skin, show_markings = params[1:] |
558
|
|
|
|
559
|
1 |
|
is_restorable = allows_spawning__restorable == IS_STATIONARY_AIRCRAFT_RESTORABLE |
560
|
1 |
|
skin = None if skin == NULL else skin |
561
|
|
|
|
562
|
1 |
|
return { |
563
|
|
|
'air_force': to_air_force(air_force), |
564
|
|
|
'allows_spawning': to_bool(allows_spawning__restorable), |
565
|
|
|
'is_restorable': is_restorable, |
566
|
|
|
'skin': skin, |
567
|
|
|
'show_markings': to_bool(show_markings), |
568
|
|
|
} |
569
|
|
|
|
570
|
1 |
|
def __parse_ship(params): |
571
|
|
|
""" |
572
|
|
|
Parse additional options for ``ships`` type |
573
|
|
|
""" |
574
|
1 |
|
awakening_time, skill, recharge_time = params[1:] |
575
|
1 |
|
return { |
576
|
|
|
'awakening_time': float(awakening_time), |
577
|
|
|
'recharge_time': float(recharge_time), |
578
|
|
|
'skill': to_skill(skill), |
579
|
|
|
} |
580
|
|
|
|
581
|
1 |
|
subparsers = { |
582
|
|
|
'artillery': (StationaryArtillery, __parse_artillery), |
583
|
|
|
'planes': (StationaryAircraft, __parse_aircraft), |
584
|
|
|
'ships': (StationaryShip, __parse_ship), |
585
|
|
|
} |
586
|
|
|
|
587
|
1 |
|
def check_section_name(self, section_name): |
588
|
1 |
|
return section_name == "NStationary" |
589
|
|
|
|
590
|
1 |
|
def parse_line(self, line): |
591
|
1 |
|
params = line.split() |
592
|
|
|
|
593
|
1 |
|
oid, object_name, belligerent = params[0], params[1], params[2] |
594
|
1 |
|
pos = params[3:5] |
595
|
1 |
|
rotation_angle = params[5] |
596
|
1 |
|
params = params[6:] |
597
|
|
|
|
598
|
1 |
|
type_name = self._get_type_name(object_name) |
599
|
1 |
|
try: |
600
|
1 |
|
object_type = to_unit_type(type_name) |
601
|
1 |
|
except Exception: |
602
|
|
|
# Use original string as object's type |
603
|
1 |
|
object_type = type_name |
604
|
|
|
|
605
|
1 |
|
the_object = { |
606
|
|
|
'id': oid, |
607
|
|
|
'belligerent': to_belligerent(belligerent), |
608
|
|
|
'code': self._get_code(object_name), |
609
|
|
|
'pos': Point2D(*pos), |
610
|
|
|
'rotation_angle': float(rotation_angle), |
611
|
|
|
'type': object_type, |
612
|
|
|
} |
613
|
|
|
|
614
|
1 |
|
if type_name in self.subparsers: |
615
|
1 |
|
structure, subparser = self.subparsers.get(type_name) |
616
|
1 |
|
the_object.update(subparser(params)) |
617
|
|
|
else: |
618
|
1 |
|
structure = StationaryObject |
619
|
|
|
|
620
|
1 |
|
self.data.append(structure(**the_object)) |
621
|
|
|
|
622
|
1 |
|
def _get_type_name(self, object_name): |
623
|
1 |
|
if object_name.startswith('ships'): |
624
|
1 |
|
return "ships" |
625
|
|
|
else: |
626
|
1 |
|
start = object_name.index('.') + 1 |
627
|
1 |
|
stop = object_name.rindex('.') |
628
|
1 |
|
return object_name[start:stop] |
629
|
|
|
|
630
|
1 |
|
def _get_code(self, code): |
631
|
1 |
|
start = code.index('$') + 1 |
632
|
1 |
|
return code[start:] |
633
|
|
|
|
634
|
1 |
|
def clean(self): |
635
|
1 |
|
return {'stationary': self.data, } |
636
|
|
|
|
637
|
|
|
|
638
|
1 |
|
class BuildingsParser(CollectingParser): |
639
|
|
|
""" |
640
|
|
|
Parses ``Buildings`` section. |
641
|
|
|
View :ref:`detailed description <buildings-section>`. |
642
|
|
|
""" |
643
|
|
|
|
644
|
1 |
|
def check_section_name(self, section_name): |
645
|
1 |
|
return section_name == "Buildings" |
646
|
|
|
|
647
|
1 |
|
def parse_line(self, line): |
648
|
1 |
|
params = line.split() |
649
|
1 |
|
oid, building_object, belligerent = params[:3] |
650
|
1 |
|
pos_x, pos_y, rotation_angle = params[3:] |
651
|
1 |
|
code = building_object.split('$')[1] |
652
|
1 |
|
self.data.append(Building( |
653
|
|
|
id=oid, |
654
|
|
|
belligerent=to_belligerent(belligerent), |
655
|
|
|
code=code, |
656
|
|
|
pos=Point2D(pos_x, pos_y), |
657
|
|
|
rotation_angle=float(rotation_angle), |
658
|
|
|
)) |
659
|
|
|
|
660
|
1 |
|
def clean(self): |
661
|
1 |
|
return {'buildings': self.data, } |
662
|
|
|
|
663
|
|
|
|
664
|
1 |
|
class TargetParser(CollectingParser): |
665
|
|
|
""" |
666
|
|
|
Parses ``Target`` section. |
667
|
|
|
View :ref:`detailed description <target-section>`. |
668
|
|
|
""" |
669
|
|
|
|
670
|
1 |
|
def check_section_name(self, section_name): |
671
|
1 |
|
return section_name == "Target" |
672
|
|
|
|
673
|
1 |
|
def parse_line(self, line): |
674
|
1 |
|
params = line.split() |
675
|
|
|
|
676
|
1 |
|
type_code, priority, in_sleep_mode, delay = params[:4] |
677
|
1 |
|
params = params[4:] |
678
|
|
|
|
679
|
1 |
|
target_type = TargetTypes.get_by_value(int(type_code)) |
680
|
1 |
|
target = { |
681
|
|
|
'type': target_type, |
682
|
|
|
'priority': TargetPriorities.get_by_value(int(priority)), |
683
|
|
|
'in_sleep_mode': to_bool(in_sleep_mode), |
684
|
|
|
'delay': int(delay), |
685
|
|
|
} |
686
|
|
|
|
687
|
1 |
|
subparser = TargetParser._subparsers.get(target_type) |
688
|
1 |
|
if subparser is not None: |
689
|
1 |
|
target.update(subparser(params)) |
690
|
|
|
|
691
|
1 |
|
self.data.append(target) |
692
|
|
|
|
693
|
1 |
|
@staticmethod |
694
|
|
|
def to_destruction_level(value): |
695
|
1 |
|
return int(value) / 10 |
696
|
|
|
|
697
|
1 |
|
def parse_destroy_or_cover_or_escort(params): |
698
|
|
|
""" |
699
|
|
|
Parse extra parameters for targets with type 'destroy' or 'cover' or |
700
|
|
|
'escort'. |
701
|
|
|
""" |
702
|
1 |
|
destruction_level = TargetParser.to_destruction_level(params[0]) |
703
|
1 |
|
pos, waypoint, object_code = params[1:3], params[4], params[5] |
704
|
1 |
|
object_pos = params[6:8] |
705
|
1 |
|
return { |
706
|
|
|
'destruction_level': destruction_level, |
707
|
|
|
'pos': Point2D(*pos), |
708
|
|
|
'object': { |
709
|
|
|
'waypoint': int(waypoint), |
710
|
|
|
'id': object_code, |
711
|
|
|
'pos': Point2D(*object_pos), |
712
|
|
|
}, |
713
|
|
|
} |
714
|
|
|
|
715
|
1 |
|
def parse_destroy_or_cover_bridge(params): |
716
|
|
|
""" |
717
|
|
|
Parse extra parameters for targets with type 'destroy bridge' or |
718
|
|
|
'cover bridge'. |
719
|
|
|
""" |
720
|
1 |
|
pos, object_code, object_pos = params[1:3], params[5], params[6:8] |
721
|
1 |
|
return { |
722
|
|
|
'pos': Point2D(*pos), |
723
|
|
|
'object': { |
724
|
|
|
'id': object_code, |
725
|
|
|
'pos': Point2D(*object_pos), |
726
|
|
|
}, |
727
|
|
|
} |
728
|
|
|
|
729
|
1 |
|
def parse_destroy_or_cover_area(params): |
730
|
|
|
""" |
731
|
|
|
Parse extra parameters for targets with type 'destroy area' or |
732
|
|
|
'cover area'. |
733
|
|
|
""" |
734
|
1 |
|
destruction_level = TargetParser.to_destruction_level(params[0]) |
735
|
1 |
|
pos_x, pos_y, radius = params[1:] |
736
|
1 |
|
return { |
737
|
|
|
'destruction_level': destruction_level, |
738
|
|
|
'pos': Point2D(pos_x, pos_y), |
739
|
|
|
'radius': int(radius), |
740
|
|
|
} |
741
|
|
|
|
742
|
1 |
|
def parse_recon(params): |
743
|
|
|
""" |
744
|
|
|
Parse extra parameters for targets with 'recon' type. |
745
|
|
|
""" |
746
|
1 |
|
requires_landing = params[0] != '500' |
747
|
1 |
|
pos, radius, params = params[1:3], params[3], params[4:] |
748
|
1 |
|
data = { |
749
|
|
|
'radius': int(radius), |
750
|
|
|
'requires_landing': requires_landing, |
751
|
|
|
'pos': Point2D(*pos), |
752
|
|
|
} |
753
|
1 |
|
if params: |
754
|
1 |
|
waypoint, object_code = params[:2] |
755
|
1 |
|
object_pos = params[2:] |
756
|
1 |
|
data['object'] = { |
757
|
|
|
'waypoint': int(waypoint), |
758
|
|
|
'id': object_code, |
759
|
|
|
'pos': Point2D(*object_pos), |
760
|
|
|
} |
761
|
1 |
|
return data |
762
|
|
|
|
763
|
1 |
|
_subparsers = { |
764
|
|
|
TargetTypes.destroy: parse_destroy_or_cover_or_escort, |
765
|
|
|
TargetTypes.destroy_bridge: parse_destroy_or_cover_bridge, |
766
|
|
|
TargetTypes.destroy_area: parse_destroy_or_cover_area, |
767
|
|
|
TargetTypes.recon: parse_recon, |
768
|
|
|
TargetTypes.escort: parse_destroy_or_cover_or_escort, |
769
|
|
|
TargetTypes.cover: parse_destroy_or_cover_or_escort, |
770
|
|
|
TargetTypes.cover_area: parse_destroy_or_cover_area, |
771
|
|
|
TargetTypes.cover_bridge: parse_destroy_or_cover_bridge, |
772
|
|
|
} |
773
|
|
|
|
774
|
1 |
|
def clean(self): |
775
|
1 |
|
return {'targets': self.data, } |
776
|
|
|
|
777
|
|
|
|
778
|
1 |
|
class BornPlaceParser(CollectingParser): |
779
|
|
|
""" |
780
|
|
|
Parses ``BornPlace`` section. |
781
|
|
|
View :ref:`detailed description <bornplace-section>`. |
782
|
|
|
""" |
783
|
|
|
|
784
|
1 |
|
def check_section_name(self, section_name): |
785
|
1 |
|
return section_name == "BornPlace" |
786
|
|
|
|
787
|
1 |
|
def parse_line(self, line): |
788
|
1 |
|
( |
789
|
|
|
belligerent, the_range, pos_x, pos_y, has_parachutes, |
790
|
|
|
air_spawn_height, air_spawn_speed, air_spawn_heading, max_pilots, |
791
|
|
|
radar_min_height, radar_max_height, radar_range, air_spawn_always, |
792
|
|
|
enable_aircraft_limits, aircraft_limits_consider_lost, |
793
|
|
|
disable_spawning, friction_enabled, friction_value, |
794
|
|
|
aircraft_limits_consider_stationary, show_default_icon, |
795
|
|
|
air_spawn_if_deck_is_full, spawn_in_stationary, |
796
|
|
|
return_to_start_position |
797
|
|
|
) = line.split() |
798
|
|
|
|
799
|
1 |
|
self.data.append({ |
800
|
|
|
'range': int(the_range), |
801
|
|
|
'belligerent': to_belligerent(belligerent), |
802
|
|
|
'show_default_icon': to_bool(show_default_icon), |
803
|
|
|
'friction': { |
804
|
|
|
'enabled': to_bool(friction_enabled), |
805
|
|
|
'value': float(friction_value), |
806
|
|
|
}, |
807
|
|
|
'spawning': { |
808
|
|
|
'enabled': not to_bool(disable_spawning), |
809
|
|
|
'with_parachutes': to_bool(has_parachutes), |
810
|
|
|
'max_pilots': int(max_pilots), |
811
|
|
|
'in_stationary': { |
812
|
|
|
'enabled': to_bool(spawn_in_stationary), |
813
|
|
|
'return_to_start_position': to_bool(return_to_start_position), |
814
|
|
|
}, |
815
|
|
|
'in_air': { |
816
|
|
|
'height': int(air_spawn_height), |
817
|
|
|
'speed': int(air_spawn_speed), |
818
|
|
|
'heading': int(air_spawn_heading), |
819
|
|
|
'conditions': { |
820
|
|
|
'always': to_bool(air_spawn_always), |
821
|
|
|
'if_deck_is_full': to_bool(air_spawn_if_deck_is_full), |
822
|
|
|
}, |
823
|
|
|
}, |
824
|
|
|
'aircraft_limitations': { |
825
|
|
|
'enabled': to_bool(enable_aircraft_limits), |
826
|
|
|
'consider_lost': to_bool(aircraft_limits_consider_lost), |
827
|
|
|
'consider_stationary': to_bool(aircraft_limits_consider_stationary), |
828
|
|
|
}, |
829
|
|
|
}, |
830
|
|
|
'radar': { |
831
|
|
|
'range': int(radar_range), |
832
|
|
|
'min_height': int(radar_min_height), |
833
|
|
|
'max_height': int(radar_max_height), |
834
|
|
|
}, |
835
|
|
|
'pos': Point2D(pos_x, pos_y), |
836
|
|
|
}) |
837
|
|
|
|
838
|
1 |
|
def clean(self): |
839
|
1 |
|
return {'home_bases': self.data, } |
840
|
|
|
|
841
|
|
|
|
842
|
1 |
|
class BornPlaceAircraftsParser(CollectingParser): |
843
|
|
|
""" |
844
|
|
|
Parses ``BornPlaceN`` section. |
845
|
|
|
View :ref:`detailed description <bornplace-aircrafts-section>`. |
846
|
|
|
""" |
847
|
1 |
|
input_prefix = 'BornPlace' |
848
|
1 |
|
output_prefix = 'home_base_aircrafts_' |
849
|
|
|
|
850
|
1 |
|
def check_section_name(self, section_name): |
851
|
1 |
|
if not section_name.startswith(self.input_prefix): |
852
|
1 |
|
return False |
853
|
1 |
|
try: |
854
|
1 |
|
self._extract_section_number(section_name) |
855
|
1 |
|
except ValueError: |
856
|
1 |
|
return False |
857
|
|
|
else: |
858
|
1 |
|
return True |
859
|
|
|
|
860
|
1 |
|
def init_parser(self, section_name): |
861
|
1 |
|
super(BornPlaceAircraftsParser, self).init_parser(section_name) |
862
|
1 |
|
self.output_key = ( |
863
|
|
|
"{}{}".format(self.output_prefix, |
864
|
|
|
self._extract_section_number(section_name))) |
865
|
1 |
|
self.aircraft = None |
866
|
|
|
|
867
|
1 |
|
def _extract_section_number(self, section_name): |
868
|
1 |
|
start = len(self.input_prefix) |
869
|
1 |
|
return int(section_name[start:]) |
870
|
|
|
|
871
|
1 |
|
def parse_line(self, line): |
872
|
1 |
|
parts = line.split() |
873
|
|
|
|
874
|
1 |
|
if parts[0] == WEAPONS_CONTINUATION_MARK: |
875
|
1 |
|
self.aircraft['weapon_limitations'].extend(parts[1:]) |
876
|
|
|
else: |
877
|
1 |
|
if self.aircraft: |
878
|
|
|
# Finalize previous aircraft |
879
|
1 |
|
self.data.append(self.aircraft) |
880
|
1 |
|
self.aircraft = BornPlaceAircraftsParser._parse_new_item(parts) |
881
|
|
|
|
882
|
1 |
|
@staticmethod |
883
|
|
|
def _parse_new_item(parts): |
884
|
1 |
|
code = parts.pop(0) |
885
|
1 |
|
limit = BornPlaceAircraftsParser._extract_limit(parts) |
886
|
1 |
|
return { |
887
|
|
|
'code': code, |
888
|
|
|
'limit': limit, |
889
|
|
|
'weapon_limitations': parts, |
890
|
|
|
} |
891
|
|
|
|
892
|
1 |
|
@staticmethod |
893
|
|
|
def _extract_limit(parts): |
894
|
1 |
|
if parts: |
895
|
1 |
|
limit = int(parts.pop(0)) |
896
|
1 |
|
limit = limit if limit >= 0 else None |
897
|
|
|
else: |
898
|
1 |
|
limit = None |
899
|
1 |
|
return limit |
900
|
|
|
|
901
|
1 |
|
def clean(self): |
902
|
1 |
|
if self.aircraft: |
903
|
1 |
|
aircraft, self.aircraft = self.aircraft, None |
904
|
1 |
|
self.data.append(aircraft) |
905
|
|
|
|
906
|
1 |
|
return {self.output_key: self.data, } |
907
|
|
|
|
908
|
|
|
|
909
|
1 |
|
class BornPlaceAirForcesParser(CollectingParser): |
910
|
|
|
""" |
911
|
|
|
Parses ``BornPlaceCountriesN`` section. |
912
|
|
|
View :ref:`detailed description <bornplace-air-forces-section>`. |
913
|
|
|
""" |
914
|
1 |
|
input_prefix = 'BornPlaceCountries' |
915
|
1 |
|
output_prefix = 'home_base_air_forces_' |
916
|
|
|
|
917
|
1 |
|
def check_section_name(self, section_name): |
918
|
1 |
|
if not section_name.startswith(self.input_prefix): |
919
|
1 |
|
return False |
920
|
1 |
|
try: |
921
|
1 |
|
self._extract_section_number(section_name) |
922
|
1 |
|
except ValueError: |
923
|
1 |
|
return False |
924
|
|
|
else: |
925
|
1 |
|
return True |
926
|
|
|
|
927
|
1 |
|
def init_parser(self, section_name): |
928
|
1 |
|
super(BornPlaceAirForcesParser, self).init_parser(section_name) |
929
|
1 |
|
self.output_key = ( |
930
|
|
|
"{}{}".format(self.output_prefix, |
931
|
|
|
self._extract_section_number(section_name))) |
932
|
1 |
|
self.countries = {} |
933
|
|
|
|
934
|
1 |
|
def _extract_section_number(self, section_name): |
935
|
1 |
|
start = len(self.input_prefix) |
936
|
1 |
|
return int(section_name[start:]) |
937
|
|
|
|
938
|
1 |
|
def parse_line(self, line): |
939
|
1 |
|
air_force = to_air_force(line.strip()) |
940
|
1 |
|
self.data.append(air_force) |
941
|
|
|
|
942
|
1 |
|
def clean(self): |
943
|
1 |
|
return {self.output_key: self.data, } |
944
|
|
|
|
945
|
|
|
|
946
|
1 |
|
class StaticCameraParser(CollectingParser): |
947
|
|
|
""" |
948
|
|
|
Parses ``StaticCamera`` section. |
949
|
|
|
View :ref:`detailed description <static-camera-section>`. |
950
|
|
|
""" |
951
|
|
|
|
952
|
1 |
|
def check_section_name(self, section_name): |
953
|
1 |
|
return section_name == "StaticCamera" |
954
|
|
|
|
955
|
1 |
|
def parse_line(self, line): |
956
|
1 |
|
pos_x, pos_y, pos_z, belligerent = line.split() |
957
|
1 |
|
self.data.append(StaticCamera( |
958
|
|
|
belligerent=to_belligerent(belligerent), |
959
|
|
|
pos=Point3D(pos_x, pos_y, pos_z), |
960
|
|
|
)) |
961
|
|
|
|
962
|
1 |
|
def clean(self): |
963
|
1 |
|
return {'cameras': self.data, } |
964
|
|
|
|
965
|
|
|
|
966
|
1 |
|
class FrontMarkerParser(CollectingParser): |
967
|
|
|
""" |
968
|
|
|
Parses ``FrontMarker`` section. |
969
|
|
|
View :ref:`detailed description <front-marker-section>`. |
970
|
|
|
""" |
971
|
|
|
|
972
|
1 |
|
def check_section_name(self, section_name): |
973
|
1 |
|
return section_name == "FrontMarker" |
974
|
|
|
|
975
|
1 |
|
def parse_line(self, line): |
976
|
1 |
|
oid, pos_x, pos_y, belligerent = line.split() |
977
|
1 |
|
self.data.append(FrontMarker( |
978
|
|
|
id=oid, |
979
|
|
|
belligerent=to_belligerent(belligerent), |
980
|
|
|
pos=Point2D(pos_x, pos_y), |
981
|
|
|
)) |
982
|
|
|
|
983
|
1 |
|
def clean(self): |
984
|
1 |
|
return {'markers': self.data, } |
985
|
|
|
|
986
|
|
|
|
987
|
1 |
|
class RocketParser(CollectingParser): |
988
|
|
|
""" |
989
|
|
|
Parses ``Rocket`` section. |
990
|
|
|
View :ref:`detailed description <rocket-section>`. |
991
|
|
|
""" |
992
|
|
|
|
993
|
1 |
|
def check_section_name(self, section_name): |
994
|
1 |
|
return section_name == "Rocket" |
995
|
|
|
|
996
|
1 |
|
def parse_line(self, line): |
997
|
1 |
|
params = line.split() |
998
|
|
|
|
999
|
1 |
|
oid, code, belligerent = params[0:3] |
1000
|
1 |
|
pos = params[3:5] |
1001
|
1 |
|
rotation_angle, delay, count, period = params[5:9] |
1002
|
1 |
|
destination = params[9:] |
1003
|
|
|
|
1004
|
1 |
|
self.data.append(Rocket( |
1005
|
|
|
id=oid, |
1006
|
|
|
code=code, |
1007
|
|
|
belligerent=to_belligerent(belligerent), |
1008
|
|
|
pos=Point2D(*pos), |
1009
|
|
|
rotation_angle=float(rotation_angle), |
1010
|
|
|
delay=float(delay), |
1011
|
|
|
count=int(count), |
1012
|
|
|
period=float(period), |
1013
|
|
|
destination=Point2D(*destination) if destination else None |
1014
|
|
|
)) |
1015
|
|
|
|
1016
|
1 |
|
def clean(self): |
1017
|
1 |
|
return {'rockets': self.data} |
1018
|
|
|
|
1019
|
|
|
|
1020
|
1 |
|
class WingParser(CollectingParser): |
1021
|
|
|
""" |
1022
|
|
|
Parses ``Wing`` section. |
1023
|
|
|
View :ref:`detailed description <wing-section>`. |
1024
|
|
|
""" |
1025
|
|
|
|
1026
|
1 |
|
def check_section_name(self, section_name): |
1027
|
1 |
|
return section_name == "Wing" |
1028
|
|
|
|
1029
|
1 |
|
def clean(self): |
1030
|
1 |
|
return {'flights': self.data} |
1031
|
|
|
|
1032
|
|
|
|
1033
|
1 |
|
class FlightInfoParser(ValuesParser): |
1034
|
|
|
""" |
1035
|
|
|
Parses settings for a moving flight group. |
1036
|
|
|
View :ref:`detailed description <flight-info-section>`. |
1037
|
|
|
""" |
1038
|
|
|
|
1039
|
1 |
|
def check_section_name(self, section_name): |
1040
|
1 |
|
try: |
1041
|
1 |
|
self._decompose_section_name(section_name) |
1042
|
1 |
|
except Exception: |
1043
|
1 |
|
return False |
1044
|
|
|
else: |
1045
|
1 |
|
return True |
1046
|
|
|
|
1047
|
1 |
|
def init_parser(self, section_name): |
1048
|
1 |
|
super(FlightInfoParser, self).init_parser(section_name) |
1049
|
1 |
|
self.output_key = section_name |
1050
|
1 |
|
self.flight_info = self._decompose_section_name(section_name) |
1051
|
|
|
|
1052
|
1 |
|
def _decompose_section_name(self, section_name): |
1053
|
1 |
|
prefix = section_name[:-2] |
1054
|
1 |
|
squadron, flight = section_name[-2:] |
1055
|
|
|
|
1056
|
1 |
|
try: |
1057
|
1 |
|
regiment = None |
1058
|
1 |
|
air_force = AirForces.get_by_flight_prefix(prefix) |
1059
|
1 |
|
except ValueError: |
1060
|
1 |
|
regiment = Regiments.get_by_code_name(prefix) |
1061
|
1 |
|
air_force = regiment.air_force |
1062
|
|
|
|
1063
|
1 |
|
return { |
1064
|
|
|
'id': section_name, |
1065
|
|
|
'air_force': air_force, |
1066
|
|
|
'regiment': regiment, |
1067
|
|
|
'squadron_index': int(squadron), |
1068
|
|
|
'flight_index': int(flight), |
1069
|
|
|
} |
1070
|
|
|
|
1071
|
1 |
|
def clean(self): |
1072
|
1 |
|
count = int(self.data['Planes']) |
1073
|
1 |
|
code = self.data['Class'].split('.', 1)[1] |
1074
|
1 |
|
aircrafts = [] |
1075
|
|
|
|
1076
|
1 |
|
def _add_if_present(target, key, value): |
1077
|
1 |
|
if value: |
1078
|
1 |
|
target[key] = value |
1079
|
|
|
|
1080
|
1 |
|
for i in range(count): |
1081
|
1 |
|
aircraft = { |
1082
|
|
|
'index': i, |
1083
|
|
|
'has_markings': self._has_markings(i), |
1084
|
|
|
'skill': self._get_skill(i), |
1085
|
|
|
} |
1086
|
1 |
|
_add_if_present( |
1087
|
|
|
aircraft, 'aircraft_skin', self._get_skin('skin', i)) |
1088
|
1 |
|
_add_if_present( |
1089
|
|
|
aircraft, 'nose_art', self._get_skin('nose_art', i)) |
1090
|
1 |
|
_add_if_present( |
1091
|
|
|
aircraft, 'pilot_skin', self._get_skin('pilot', i)) |
1092
|
1 |
|
_add_if_present( |
1093
|
|
|
aircraft, 'spawn_object', self._get_spawn_object_id(i)) |
1094
|
1 |
|
aircrafts.append(aircraft) |
1095
|
|
|
|
1096
|
1 |
|
self.flight_info.update({ |
1097
|
|
|
'ai_only': 'OnlyAI' in self.data, |
1098
|
|
|
'aircrafts': aircrafts, |
1099
|
|
|
'code': code, |
1100
|
|
|
'fuel': int(self.data['Fuel']), |
1101
|
|
|
'with_parachutes': 'Parachute' not in self.data, |
1102
|
|
|
'count': count, |
1103
|
|
|
'weapons': self.data['weapons'], |
1104
|
|
|
}) |
1105
|
|
|
|
1106
|
1 |
|
return {self.output_key: self.flight_info} |
1107
|
|
|
|
1108
|
1 |
|
def _get_skill(self, aircraft_id): |
1109
|
1 |
|
if 'Skill' in self.data: |
1110
|
1 |
|
return to_skill(self.data['Skill']) |
1111
|
|
|
else: |
1112
|
1 |
|
return to_skill(self.data['Skill{:}'.format(aircraft_id)]) |
1113
|
|
|
|
1114
|
1 |
|
def _has_markings(self, aircraft_id): |
1115
|
1 |
|
return 'numberOn{:}'.format(aircraft_id) not in self.data |
1116
|
|
|
|
1117
|
1 |
|
def _get_skin(self, prefix, aircraft_id): |
1118
|
1 |
|
return self.data.get('{:}{:}'.format(prefix, aircraft_id)) |
1119
|
|
|
|
1120
|
1 |
|
def _get_spawn_object_id(self, aircraft_id): |
1121
|
1 |
|
return self.data.get('spawn{:}'.format(aircraft_id)) |
1122
|
|
|
|
1123
|
|
|
|
1124
|
1 |
|
class FlightRouteParser(CollectingParser): |
1125
|
|
|
""" |
1126
|
|
|
Parses ``*_Way`` section. |
1127
|
|
|
View :ref:`detailed description <flight-route-section>`. |
1128
|
|
|
""" |
1129
|
1 |
|
input_suffix = "_Way" |
1130
|
1 |
|
output_prefix = 'flight_route_' |
1131
|
|
|
|
1132
|
1 |
|
def check_section_name(self, section_name): |
1133
|
1 |
|
return section_name.endswith(self.input_suffix) |
1134
|
|
|
|
1135
|
1 |
|
def _extract_flight_code(self, section_name): |
1136
|
1 |
|
return section_name[:-len(self.input_suffix)] |
1137
|
|
|
|
1138
|
1 |
|
def init_parser(self, section_name): |
1139
|
1 |
|
super(FlightRouteParser, self).init_parser(section_name) |
1140
|
1 |
|
flight_code = self._extract_flight_code(section_name) |
1141
|
1 |
|
self.output_key = "{}{}".format(self.output_prefix, flight_code) |
1142
|
1 |
|
self.point = None |
1143
|
1 |
|
self.point_class = None |
1144
|
|
|
|
1145
|
1 |
|
def parse_line(self, line): |
1146
|
1 |
|
params = line.split() |
1147
|
1 |
|
type_code, params = params[0], params[1:] |
1148
|
1 |
|
if type_code == ROUTE_POINT_EXTRA_PARAMETERS_MARK: |
1149
|
1 |
|
self._parse_options(params) |
1150
|
|
|
else: |
1151
|
1 |
|
self._finalize_current_point() |
1152
|
1 |
|
pos, speed, params = params[0:3], params[3], params[4:] |
1153
|
1 |
|
self.point = { |
1154
|
|
|
'type': RoutePointTypes.get_by_value(type_code), |
1155
|
|
|
'pos': Point3D(*pos), |
1156
|
|
|
'speed': float(speed), |
1157
|
|
|
} |
1158
|
1 |
|
self._parse_extra(params) |
1159
|
|
|
|
1160
|
1 |
|
def _parse_options(self, params): |
1161
|
1 |
|
try: |
1162
|
1 |
|
cycles, timeout, angle, side_size, altitude_difference = params |
1163
|
1 |
|
self.point.update({ |
1164
|
|
|
'patrol_cycles': int(cycles), |
1165
|
|
|
'patrol_timeout': int(timeout), |
1166
|
|
|
'pattern_angle': int(angle), |
1167
|
|
|
'pattern_side_size': int(side_size), |
1168
|
|
|
'pattern_altitude_difference': int(altitude_difference), |
1169
|
|
|
}) |
1170
|
1 |
|
self.point_class = FlightRoutePatrolPoint |
1171
|
1 |
|
except ValueError: |
1172
|
1 |
|
delay, spacing = params[1:3] |
1173
|
1 |
|
self.point.update({ |
1174
|
|
|
'delay': int(delay), |
1175
|
|
|
'spacing': int(spacing), |
1176
|
|
|
}) |
1177
|
1 |
|
self.point_class = FlightRouteTakeoffPoint |
1178
|
|
|
|
1179
|
1 |
|
def _parse_extra(self, params): |
1180
|
1 |
|
if FlightRouteParser._is_new_game_version(params): |
1181
|
1 |
|
radio_silence, formation, params = FlightRouteParser._parse_new_version_extra(params) |
1182
|
1 |
|
if params: |
1183
|
1 |
|
self._parse_target(params) |
1184
|
|
|
else: |
1185
|
1 |
|
radio_silence = False |
1186
|
1 |
|
formation = None |
1187
|
|
|
|
1188
|
1 |
|
self.point.update({ |
1189
|
|
|
'radio_silence': radio_silence, |
1190
|
|
|
'formation': formation, |
1191
|
|
|
}) |
1192
|
|
|
|
1193
|
1 |
|
@staticmethod |
1194
|
|
|
def _is_new_game_version(params): |
1195
|
1 |
|
return ( |
1196
|
|
|
ROUTE_POINT_RADIO_SILENCE_ON in params |
1197
|
|
|
or ROUTE_POINT_RADIO_SILENCE_OFF in params |
1198
|
|
|
) |
1199
|
|
|
|
1200
|
1 |
|
@staticmethod |
1201
|
|
|
def _parse_new_version_extra(params): |
1202
|
1 |
|
try: |
1203
|
1 |
|
index = params.index(ROUTE_POINT_RADIO_SILENCE_ON) |
1204
|
1 |
|
except ValueError: |
1205
|
1 |
|
index = params.index(ROUTE_POINT_RADIO_SILENCE_OFF) |
1206
|
|
|
|
1207
|
1 |
|
params, radio_silence, extra = params[:index], params[index], params[index+1:] |
1208
|
|
|
|
1209
|
1 |
|
radio_silence = radio_silence == ROUTE_POINT_RADIO_SILENCE_ON |
1210
|
1 |
|
formation = Formations.get_by_value(extra[0]) if extra else None |
1211
|
|
|
|
1212
|
1 |
|
return radio_silence, formation, params |
1213
|
|
|
|
1214
|
1 |
|
def _parse_target(self, params): |
1215
|
1 |
|
target_id, target_route_point = params[:2] |
1216
|
|
|
|
1217
|
1 |
|
self.point.update({ |
1218
|
|
|
'target_id': target_id, |
1219
|
|
|
'target_route_point': int(target_route_point), |
1220
|
|
|
}) |
1221
|
|
|
|
1222
|
1 |
|
if self.point['type'] is RoutePointTypes.normal: |
1223
|
1 |
|
self.point['type'] = RoutePointTypes.air_attack |
1224
|
|
|
|
1225
|
1 |
|
self.point_class = FlightRouteAttackPoint |
1226
|
|
|
|
1227
|
1 |
|
def clean(self): |
1228
|
1 |
|
self._finalize_current_point() |
1229
|
1 |
|
return {self.output_key: self.data} |
1230
|
|
|
|
1231
|
1 |
|
def _finalize_current_point(self): |
1232
|
1 |
|
if self.point: |
1233
|
1 |
|
point_class = getattr(self, 'point_class') or FlightRoutePoint |
1234
|
1 |
|
self.data.append(point_class(**self.point)) |
1235
|
1 |
|
self.point = None |
1236
|
1 |
|
self.point_class = None |
1237
|
|
|
|
1238
|
|
|
|
1239
|
1 |
|
class FileParser(object): |
1240
|
|
|
""" |
1241
|
|
|
Parses a whole mission file. |
1242
|
|
|
View :ref:`detailed description <file-parser>`. |
1243
|
|
|
""" |
1244
|
|
|
|
1245
|
1 |
|
def __init__(self): |
1246
|
1 |
|
self.parsers = [ |
1247
|
|
|
MainParser(), |
1248
|
|
|
SeasonParser(), |
1249
|
|
|
WeatherParser(), |
1250
|
|
|
RespawnTimeParser(), |
1251
|
|
|
MDSParser(), |
1252
|
|
|
MDSScoutsParser(), |
1253
|
|
|
ChiefsParser(), |
1254
|
|
|
ChiefRoadParser(), |
1255
|
|
|
NStationaryParser(), |
1256
|
|
|
BuildingsParser(), |
1257
|
|
|
TargetParser(), |
1258
|
|
|
BornPlaceParser(), |
1259
|
|
|
BornPlaceAircraftsParser(), |
1260
|
|
|
BornPlaceAirForcesParser(), |
1261
|
|
|
StaticCameraParser(), |
1262
|
|
|
FrontMarkerParser(), |
1263
|
|
|
RocketParser(), |
1264
|
|
|
WingParser(), |
1265
|
|
|
FlightRouteParser(), |
1266
|
|
|
] |
1267
|
1 |
|
self.flight_info_parser = FlightInfoParser() |
1268
|
|
|
|
1269
|
1 |
|
def parse(self, mission): |
1270
|
1 |
|
if isinstance(mission, six.string_types): |
1271
|
1 |
|
with open(mission, 'r') as f: |
1272
|
1 |
|
return self.parse_stream(f) |
1273
|
|
|
else: |
1274
|
1 |
|
return self.parse_stream(mission) |
1275
|
|
|
|
1276
|
1 |
|
def parse_stream(self, sequence): |
1277
|
1 |
|
self._current_parser = None |
1278
|
1 |
|
self.data = {} |
1279
|
|
|
|
1280
|
1 |
|
for i, line in enumerate(sequence): |
1281
|
1 |
|
line = strip_comments(line) |
1282
|
1 |
|
if FileParser.is_section_name(line): |
1283
|
1 |
|
self._finalize_current_parser() |
1284
|
1 |
|
section_name = FileParser.get_section_name(line) |
1285
|
1 |
|
self._current_parser = self._get_parser(section_name) |
1286
|
1 |
|
elif self._current_parser: |
1287
|
1 |
|
self._try_to_parse_line(i, line) |
1288
|
|
|
|
1289
|
1 |
|
self._finalize_current_parser() |
1290
|
1 |
|
return self._clean() |
1291
|
|
|
|
1292
|
1 |
|
@staticmethod |
1293
|
|
|
def is_section_name(line): |
1294
|
1 |
|
return line.startswith('[') and line.endswith(']') |
1295
|
|
|
|
1296
|
1 |
|
@staticmethod |
1297
|
|
|
def get_section_name(line): |
1298
|
1 |
|
return line.strip('[]') |
1299
|
|
|
|
1300
|
1 |
|
def _get_parser(self, section_name): |
1301
|
1 |
|
parser = self.flight_info_parser |
1302
|
1 |
|
flights = self.data.get('flights') |
1303
|
|
|
|
1304
|
1 |
|
if flights is not None and parser.start(section_name): |
1305
|
1 |
|
return parser |
1306
|
|
|
|
1307
|
1 |
|
for parser in self.parsers: |
1308
|
1 |
|
if parser.start(section_name): |
1309
|
1 |
|
return parser |
1310
|
|
|
|
1311
|
1 |
|
return None |
1312
|
|
|
|
1313
|
1 |
|
def _finalize_current_parser(self): |
1314
|
1 |
|
if not self._current_parser: |
1315
|
1 |
|
return |
1316
|
1 |
|
try: |
1317
|
1 |
|
data = self._current_parser.stop() |
1318
|
1 |
|
except Exception: |
1319
|
1 |
|
error_type, original_msg, traceback = sys.exc_info() |
1320
|
1 |
|
msg = ( |
1321
|
|
|
"{0} during finalization of \"{1}\": {2}" |
1322
|
|
|
.format(error_type.__name__, |
1323
|
|
|
self._current_parser.__class__.__name__, |
1324
|
|
|
original_msg)) |
1325
|
1 |
|
FileParser._raise_error(msg, traceback) |
1326
|
|
|
else: |
1327
|
1 |
|
self.data.update(data) |
1328
|
|
|
finally: |
1329
|
1 |
|
self._current_parser = None |
1330
|
|
|
|
1331
|
1 |
|
def _try_to_parse_line(self, line_number, line): |
1332
|
1 |
|
try: |
1333
|
1 |
|
self._current_parser.parse_line(line) |
1334
|
1 |
|
except Exception: |
1335
|
1 |
|
error_type, original_msg, traceback = sys.exc_info() |
1336
|
1 |
|
msg = ( |
1337
|
|
|
"{0} in line #{1} (\"{2}\"): {3}" |
1338
|
|
|
.format(error_type.__name__, line_number, line, original_msg)) |
1339
|
1 |
|
FileParser._raise_error(msg, traceback) |
1340
|
|
|
|
1341
|
1 |
|
@staticmethod |
1342
|
|
|
def _raise_error(message, traceback): |
1343
|
1 |
|
error = MissionParsingError(message) |
1344
|
1 |
|
six.reraise(MissionParsingError, error, traceback) |
1345
|
|
|
|
1346
|
1 |
|
def _clean(self): |
1347
|
1 |
|
result = {} |
1348
|
|
|
|
1349
|
1 |
|
move_if_present(result, self.data, 'location_loader') |
1350
|
1 |
|
move_if_present(result, self.data, 'player') |
1351
|
1 |
|
move_if_present(result, self.data, 'targets') |
1352
|
|
|
|
1353
|
1 |
|
set_if_present(result, 'conditions', self._get_conditions()) |
1354
|
1 |
|
set_if_present(result, 'objects', self._get_objects()) |
1355
|
|
|
|
1356
|
1 |
|
return result |
1357
|
|
|
|
1358
|
1 |
|
def _get_conditions(self): |
1359
|
1 |
|
result = {} |
1360
|
|
|
|
1361
|
1 |
|
set_if_present(result, 'time_info', self._get_time_info()) |
1362
|
1 |
|
set_if_present(result, 'meteorology', self._get_meteorology()) |
1363
|
1 |
|
set_if_present(result, 'scouting', self._get_scouting()) |
1364
|
|
|
|
1365
|
1 |
|
move_if_present(result, self.data, 'respawn_time') |
1366
|
|
|
|
1367
|
1 |
|
if 'conditions' in self.data: |
1368
|
1 |
|
conditions = self.data['conditions'] |
1369
|
|
|
|
1370
|
1 |
|
move_if_present(result, conditions, 'radar') |
1371
|
1 |
|
move_if_present(result, conditions, 'communication') |
1372
|
1 |
|
move_if_present(result, conditions, 'home_bases') |
1373
|
1 |
|
move_if_present(result, conditions, 'crater_visibility_muptipliers') |
1374
|
|
|
|
1375
|
1 |
|
return result |
1376
|
|
|
|
1377
|
1 |
|
def _get_time_info(self): |
1378
|
1 |
|
result = {} |
1379
|
|
|
|
1380
|
1 |
|
move_if_present(result, self.data, 'date') |
1381
|
1 |
|
if 'time' in self.data: |
1382
|
1 |
|
result.update({ |
1383
|
|
|
'time': self.data['time']['value'], |
1384
|
|
|
'is_fixed': self.data['time']['is_fixed'], |
1385
|
|
|
}) |
1386
|
|
|
|
1387
|
1 |
|
return result |
1388
|
|
|
|
1389
|
1 |
|
def _get_meteorology(self): |
1390
|
1 |
|
result = {} |
1391
|
|
|
|
1392
|
1 |
|
move_if_present(result, self.data, 'weather', 'weather_conditions') |
1393
|
1 |
|
move_if_present(result, self.data, 'cloud_base') |
1394
|
|
|
|
1395
|
1 |
|
if 'weather' in self.data: |
1396
|
1 |
|
result.update(self.data.pop('weather')) |
1397
|
|
|
|
1398
|
1 |
|
return result |
1399
|
|
|
|
1400
|
1 |
|
def _get_scouting(self): |
1401
|
1 |
|
result = {} |
1402
|
|
|
|
1403
|
1 |
|
try: |
1404
|
1 |
|
conditions = self.data['conditions'].pop('scouting') |
1405
|
1 |
|
result.update(conditions) |
1406
|
1 |
|
except KeyError: |
1407
|
|
|
pass |
1408
|
|
|
|
1409
|
1 |
|
keys = filter( |
1410
|
|
|
lambda x: x.startswith(MDSScoutsParser.output_prefix), |
1411
|
|
|
self.data.keys() |
1412
|
|
|
) |
1413
|
1 |
|
scouts = { |
1414
|
|
|
self.data[key]['belligerent']: self.data[key]['aircrafts'] |
1415
|
|
|
for key in keys |
1416
|
|
|
} |
1417
|
1 |
|
set_if_present(result, 'scouts', scouts) |
1418
|
|
|
|
1419
|
1 |
|
return result |
1420
|
|
|
|
1421
|
1 |
|
def _get_objects(self): |
1422
|
1 |
|
result = {} |
1423
|
|
|
|
1424
|
1 |
|
set_if_present(result, 'moving_units', self._get_moving_units()) |
1425
|
1 |
|
set_if_present(result, 'flights', self._get_flights()) |
1426
|
1 |
|
set_if_present(result, 'home_bases', self._get_home_bases()) |
1427
|
|
|
|
1428
|
1 |
|
move_if_present(result, self.data, 'stationary') |
1429
|
1 |
|
move_if_present(result, self.data, 'buildings') |
1430
|
1 |
|
move_if_present(result, self.data, 'cameras') |
1431
|
1 |
|
move_if_present(result, self.data, 'markers') |
1432
|
1 |
|
move_if_present(result, self.data, 'rockets') |
1433
|
|
|
|
1434
|
1 |
|
return result |
1435
|
|
|
|
1436
|
1 |
|
def _get_moving_units(self): |
1437
|
1 |
|
units = self.data.pop('moving_units', []) |
1438
|
1 |
|
for unit in units: |
1439
|
1 |
|
key = "{}{}".format(ChiefRoadParser.output_prefix, unit['id']) |
1440
|
1 |
|
unit['route'] = self.data.pop(key, []) |
1441
|
1 |
|
return units |
1442
|
|
|
|
1443
|
1 |
|
def _get_flights(self): |
1444
|
1 |
|
keys = self.data.pop('flights', []) |
1445
|
1 |
|
flights = [self.data.pop(key) for key in keys if key in self.data] |
1446
|
1 |
|
for flight in flights: |
1447
|
1 |
|
key = "{}{}".format(FlightRouteParser.output_prefix, flight['id']) |
1448
|
1 |
|
flight['route'] = self.data.pop(key, []) |
1449
|
1 |
|
return flights |
1450
|
|
|
|
1451
|
1 |
|
def _get_home_bases(self): |
1452
|
1 |
|
home_bases = self.data.pop('home_bases', []) |
1453
|
1 |
|
for i, home_base in enumerate(home_bases): |
1454
|
1 |
|
key = "{}{}".format(BornPlaceAircraftsParser.output_prefix, i) |
1455
|
1 |
|
home_base['spawning']['aircraft_limitations']['allowed_aircrafts'] = self.data.pop(key, []) |
1456
|
|
|
|
1457
|
1 |
|
key = "{}{}".format(BornPlaceAirForcesParser.output_prefix, i) |
1458
|
1 |
|
home_base['spawning']['allowed_air_forces'] = self.data.pop(key, []) |
1459
|
|
|
return home_bases |
1460
|
|
|
|