Completed
Push — master ( 24fe2a...025da3 )
by Oleksandr
01:08
created

il2fb.parsers.mission.clean_line()   A

Complexity

Conditions 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4
Metric Value
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.2
cc 4
crap 4
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