Skip to content

Commit e802365

Browse files
committed
First pass at basic SC2Map.MapInfo support.
Includes docs and tests. Repurposes the useless MapInfo class. Closes #126
1 parent 9b6bc4d commit e802365

File tree

8 files changed

+309
-79
lines changed

8 files changed

+309
-79
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
0.5.2 -
55
--------------------
66

7+
* Adds experimental SC2Map.MapInfo parsing support. Replaces the useless MapInfo resource from before.
78
* Summary.teams is now summary.team; summary.team is now summary.teams. To conform with replay name conventions
89
* Fixed #136, unit types from tracker events are used when available.
910
* Deprecated player.gateway for player.region

docs/source/mainobjects.rst

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,3 @@ Game Summary
2424
.. autoclass:: GameSummary
2525
:members:
2626

27-
MapInfo
28-
---------------
29-
30-
.. autoclass:: MapInfo
31-
:members:
32-
33-
MapHeader
34-
---------------
35-
36-
.. autoclass:: MapHeader
37-
:members:

docs/source/supportobjects.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,15 @@ Graph
6060

6161
.. autoclass:: Graph
6262
:members:
63+
64+
MapInfo
65+
------------------
66+
67+
.. autoclass:: MapInfo
68+
:members:
69+
70+
MapInfoPlayer
71+
------------------
72+
73+
.. autoclass:: MapInfoPlayer
74+
:members:

sc2reader/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ def setFactory(factory):
2424
module.load_map = factory.load_map
2525
module.load_game_summaries = factory.load_game_summaries
2626
module.load_game_summary = factory.load_game_summary
27-
module.load_map_infos = factory.load_map_infos
28-
module.load_map_info = factory.load_map_info
29-
module.load_map_histories = factory.load_map_headers
30-
module.load_map_history = factory.load_map_header
3127

3228
module.configure = factory.configure
3329
module.reset = factory.reset

sc2reader/factories.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,6 @@ def load_game_summaries(self, sources, options=None, **new_options):
9292
"""Loads a collection of s2gs files, returns a generator."""
9393
return self.load_all(GameSummary, sources, options, extension='s2gs', **new_options)
9494

95-
def load_map_info(self, source, options=None, **new_options):
96-
"""Loads a single s2mi file. Accepts file path, url, or file object."""
97-
return self.load(MapInfo, source, options, **new_options)
98-
99-
def load_map_infos(self, sources, options=None, **new_options):
100-
"""Loads a collection of s2mi files, returns a generator."""
101-
return self.load_all(MapInfo, sources, options, extension='s2mi', **new_options)
102-
103-
def load_map_header(self, source, options=None, **new_options):
104-
"""Loads a single s2mh file. Accepts file path, url, or file object."""
105-
return self.load(MapHeader, source, options, **new_options)
106-
107-
def load_map_headers(self, sources, options=None, **new_options):
108-
"""Loads a collection of s2mh files, returns a generator."""
109-
return self.load_all(MapHeader, sources, options, extension='s2mh', **new_options)
110-
11195
def configure(self, cls=None, **options):
11296
""" Configures the factory to use the supplied options. If cls is specified
11397
the options will only be applied when loading that class"""

sc2reader/objects.py

Lines changed: 265 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from __future__ import absolute_import
33

44
import hashlib
5-
5+
import math
66
from collections import namedtuple
77

8-
from sc2reader import utils
8+
from sc2reader import utils, log_utils
9+
from sc2reader.decoders import ByteDecoder
910
from sc2reader.constants import *
1011

1112
Location = namedtuple('Location',('x','y'))
@@ -408,3 +409,265 @@ def as_points(self):
408409
def __str__(self):
409410
return "Graph with {0} values".format(len(self.times))
410411

412+
413+
class MapInfoPlayer(object):
414+
"""
415+
Describes the player data as found in the MapInfo document of SC2Map archives.
416+
"""
417+
def __init__(self, pid, control, color, race, unknown, start_point, ai, decal):
418+
#: The pid of the player
419+
self.pid = pid
420+
421+
#: The controller of the player, one of:
422+
#:
423+
#: * 0 = Default?
424+
#: * 1 = User
425+
#: * 2 = Computer
426+
#: * 3 = Neutral
427+
#: * 4 = Hostile
428+
#: * More?
429+
#:
430+
self.control = control
431+
432+
#: The color of the player, one of:
433+
#:
434+
#: * 0xffffffff = (Any)
435+
#: * 0 = White
436+
#: * 1 = Red
437+
#: * 2 = Blue
438+
#: * 3 = Teal
439+
#: * 4 = Purple
440+
#: * 5 = Yellow
441+
#: * 6 = Orange
442+
#: * 7 = Green
443+
#: * 8 = Pink
444+
#: * 9 = Violet
445+
#: * 10 = Light Grey
446+
#: * 11 = Dark Green
447+
#: * 12 = Brown
448+
#: * 13 = Light Green
449+
#: * 14 = Dark Grey
450+
#: * 15 = Lavender
451+
#:
452+
self.color = color
453+
454+
#: The player race, "" for unset
455+
self.race = race
456+
457+
#: Unknown player setting
458+
self.unknown = unknown
459+
460+
#: The point index of the player start location; 0 = random
461+
self.start_point = start_point
462+
463+
#: The AI to use
464+
self.ai = ai
465+
466+
#: The player decal
467+
self.decal = decal
468+
469+
470+
@log_utils.loggable
471+
class MapInfo(object):
472+
"""
473+
Represents the data encoded into the MapInfo file inside every SC2Map archive
474+
"""
475+
def __init__(self, contents):
476+
# According to http://www.galaxywiki.net/MapInfo_(File_Format)
477+
# With a couple small changes for version 0x20+
478+
data = ByteDecoder(contents, endian='LITTLE')
479+
if data.read_bytes(4) != 'MapI':
480+
self.logger.warn("Invalid MapInfo file")
481+
return
482+
483+
#: The map info file format version
484+
self.version = data.read_uint32()
485+
if self.version >= 0x18:
486+
self.unknown1 = data.read_uint32()
487+
self.unknown2 = data.read_uint32()
488+
489+
#: The full map width
490+
self.width = data.read_uint32()
491+
492+
#: The full map height
493+
self.height = data.read_uint32()
494+
495+
#: Small map preview type: 0 = None, 1 = Minimap, 2 = Custom
496+
self.small_preview_type = data.read_uint32()
497+
498+
#: (Optional) Small map preview path; relative to root of map archive
499+
self.small_preview_path = str()
500+
if self.small_preview_type == 2:
501+
self.small_preview_path = data.read_cstring()
502+
503+
#: Large map preview type: 0 = None, 1 = Minimap, 2 = Custom
504+
self.large_preview_type = data.read_uint32()
505+
506+
#: (Optional) Large map preview path; relative to root of map archive
507+
self.large_preview_path = str()
508+
if self.large_preview_type == 2:
509+
self.large_preview_path = data.read_cstring()
510+
511+
if self.version >= 0x20:
512+
self.unknown3 = data.read_cstring()
513+
self.unknown4 = data.read_uint32()
514+
515+
self.unknown5 = data.read_uint32()
516+
517+
#: The type of fog of war used on the map
518+
self.fog_type = data.read_cstring()
519+
520+
#: The tile set used on the map
521+
self.tile_set = data.read_cstring()
522+
523+
#: The left bounds for the camera. This value is 7 less than the value shown in the editor.
524+
self.camera_left = data.read_uint32()
525+
526+
#: The bottom bounds for the camera. This value is 4 less than the value shown in the editor.
527+
self.camera_bottom = data.read_uint32()
528+
529+
#: The right bounds for the camera. This value is 7 more than the value shown in the editor.
530+
self.camera_right = data.read_uint32()
531+
532+
#: The top bounds for the camera. This value is 4 more than the value shown in the editor.
533+
self.camera_top = data.read_uint32()
534+
535+
#: The map base height (what is that?). This value is 4096*Base Height in the editor (giving a decimal value).
536+
self.base_height = data.read_uint32()/4096
537+
538+
#: Load screen type: 0 = default, 1 = custom
539+
self.load_screen_type = data.read_uint32()
540+
541+
#: (Optional) Load screen image path; relative to root of map archive
542+
self.load_screen_path = data.read_cstring()
543+
544+
self.unknown6 = data.read_uint16()
545+
#: Load screen image scaling strategy: 0 = normal, 1 = aspect scaling, 2 = stretch the image.
546+
self.load_screen_scaling = data.read_uint32()
547+
548+
#: The text position on the loading screen. One of:
549+
#:
550+
#: * 0xffffffff = (Default)
551+
#: * 0 = Top Left
552+
#: * 1 = Top
553+
#: * 2 = Top Right
554+
#: * 3 = Left
555+
#: * 4 = Center
556+
#: * 5 = Right
557+
#: * 6 = Bottom Left
558+
#: * 7 = Bottom
559+
#: * 8 = Bottom Right
560+
#:
561+
self.text_position = data.read_uint32()
562+
563+
#: Loading screen text position offset x
564+
self.text_position_offset_x = data.read_uint32()
565+
566+
#: Loading screen text position offset y
567+
self.text_position_offset_y = data.read_uint32()
568+
569+
#: Loading screen text size x
570+
self.text_position_size_x = data.read_uint32()
571+
572+
#: Loading screen text size y
573+
self.text_position_size_y = data.read_uint32()
574+
575+
#: A bit array of flags with the following options (possibly incomplete)
576+
#:
577+
#: * 0x00000001 = Disable Replay Recording
578+
#: * 0x00000002 = Wait for Key (Loading Screen)
579+
#: * 0x00000004 = Disable Trigger Preloading
580+
#: * 0x00000008 = Enable Story Mode Preloading
581+
#: * 0x00000010 = Use Horizontal Field of View
582+
#:
583+
self.data_flags = data.read_uint32()
584+
585+
self.unknown7 = data.read_uint32()
586+
587+
if self.version >= 0x20:
588+
self.unknown9 = data.read_bytes(21)
589+
590+
#: The number of players enabled via the data editor
591+
self.player_count = data.read_uint32()
592+
593+
#: A list of references to :class:`MapInfoPlayer` objects
594+
self.players = list()
595+
for i in range(self.player_count):
596+
self.players.append(MapInfoPlayer(
597+
pid=data.read_uint8(),
598+
control=data.read_uint32(),
599+
color=data.read_uint32(),
600+
race=data.read_cstring(),
601+
unknown=data.read_uint32(),
602+
start_point=data.read_uint32(),
603+
ai=data.read_uint32(),
604+
decal=data.read_cstring(),
605+
))
606+
607+
#: A list of the start location point indexes used in Basic Team Settings.
608+
#: The editor limits these to only Start Locations and not regular points.
609+
self.start_locations = list()
610+
for i in range(data.read_uint32()):
611+
self.start_locations.append(data.read_uint32())
612+
613+
#: The number of start locations used
614+
self.start_location_used = data.read_uint32()
615+
616+
#: The number of alliance flags encoded in :attr:`alliance_flags`.
617+
self.alliance_flags_length = data.read_uint32()
618+
# A set bit (1) indicates that the pair of Start Locations are to be allied.
619+
# bit = 1; // Set up a bitmask
620+
# // i will be the first Start Location in the Point Indexes array
621+
# // j will the the Start Location after i
622+
# for(i=0;i< Start Location Count;i++){
623+
# for(j=i+1;j < Start Location Count;j++){ // set j, and then iterate through the rest
624+
# bit <<= 1; // Shift left to move the mask to the next bit.
625+
# if((Team Enemy Flags & bit) != 0) { // These start locations are allies
626+
# // Add more to compensate for byte boundaries. This array can get big.
627+
# }
628+
# }
629+
# }
630+
#: A bit array of flags mapping out the player alliances
631+
self.alliance_flags = data.read_uint(int(math.ceil(self.alliance_flags_length/8.0)))
632+
633+
#: A list of the advanced start location point indexes used in Advanced Team Settings.
634+
#: The editor limits these to only Start Locations and not regular points.
635+
self.advanced_start_locations = list()
636+
for i in range(data.read_uint32()):
637+
# point index for each start location used
638+
self.advanced_start_locations.append(data.read_uint32())
639+
640+
#: A list of bit arrays marking which start locations below to which team.
641+
self.advanced_teams_flags = list()
642+
for i in range(data.read_uint32()):
643+
# TODO:
644+
# One set for each team. Each bit corresponds with the Point Indexes
645+
# array index (i.e., bit 0 is PointIndexes[0], bit1 is PointIndex[1],
646+
# etc.). If the bit is set, that start location is a part of that team.
647+
self.advanced_teams_flags.append(data.read_uint32())
648+
649+
#: Possibly "number of teams used"? Similar to "start locations used"
650+
self.advanced_teams_count2 = data.read_uint32()
651+
652+
#: The number of enemy flags encoded in :attr:`enemy_flags`.
653+
self.enemy_flags_length = data.read_uint32()
654+
# A set bit (1) indicates that the pair of teams are to be enemies.
655+
# bit = 1; // Set up a bitmask
656+
# // i will be the first Team in the Team Members array.
657+
# // j will be the Team that comes after i
658+
# for(i=0;i< Team Count;i++){
659+
# for(j=i+1;j < Team Count;j++){ // set j, and then iterate through the rest
660+
# bit <<= 1; // Shift left to move the mask to the next bit.
661+
# if((Team Enemy Flags & bit) != 0) { // These teams are enemies
662+
# // Add more code to compensate for byte boundaries.
663+
# }
664+
# }
665+
# }
666+
#: A bit array of flags mapping out the player enemies.
667+
self.enemy_flags = data.read_uint(int(math.ceil(self.alliance_flags_length/8.0)))
668+
669+
if data.length != data.tell():
670+
self.logger.warn("Not all of the MapInfo file was read!")
671+
672+
def __str__(self):
673+
return self.map_name

0 commit comments

Comments
 (0)