|
2 | 2 | from __future__ import absolute_import
|
3 | 3 |
|
4 | 4 | import hashlib
|
5 |
| - |
| 5 | +import math |
6 | 6 | from collections import namedtuple
|
7 | 7 |
|
8 |
| -from sc2reader import utils |
| 8 | +from sc2reader import utils, log_utils |
| 9 | +from sc2reader.decoders import ByteDecoder |
9 | 10 | from sc2reader.constants import *
|
10 | 11 |
|
11 | 12 | Location = namedtuple('Location',('x','y'))
|
@@ -408,3 +409,265 @@ def as_points(self):
|
408 | 409 | def __str__(self):
|
409 | 410 | return "Graph with {0} values".format(len(self.times))
|
410 | 411 |
|
| 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