HOME/Articles/

cnc_generals_zh_maps_manager

Article Outline

Example Python program cnc_generals_zh_maps_manager.py This program creates a PyQt GUI

Modules

  • from PyQt5.QtWidgets import QApplication, QMainWindow, QDesktopWidget, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView
  • from PyQt5.QtCore import Qt
  • from PyQt5.QtGui import QPixmap
  • from urllib.parse import unquote
  • from PIL import Image, ImageDraw
  • import logging
  • import os
  • import re
  • import sys

Classes

  • class Map:
  • class MapCacheParser:
  • class CnCGeneralsAndZeroHourMapsManager(QMainWindow):

Methods

  • def init(self):
  • def build_minimap(self):
  • def _draw_circle(self, surface, pos, diameter=6, fill=None, outline=None):
  • def repr(self):
  • def init(self, path):
  • def parse(self):
  • def search(self, num_players=None):
  • def build_minimaps(self):
  • def _process_param(self, param):
  • def _parse_coords(self, coords):
  • def _snake_case(self, name):
  • def init(self):
  • def createMapsTable(self):
  • def center(self):

Code

Example Python PyQt program :

from PyQt5.QtWidgets import QApplication, QMainWindow, QDesktopWidget, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from urllib.parse import unquote
from PIL import Image, ImageDraw
import logging
import os
import re
import sys


class Map:
    def __init__(self):
        self.techs_position = []
        self.supplies_position = []
        self.players_position = []

    def build_minimap(self):
        tga_minimap_path = self.file_path.replace('.map', '.tga')
        png_minimap_path = self.file_path.replace('.map', '.png')

        if not os.path.isfile(tga_minimap_path):
            raise FileNotFoundError('The TGA minimap image was not found: {}'.format(tga_minimap_path))

        tga_minimap = Image.open(tga_minimap_path)

        tga_minimap_draw = ImageDraw.Draw(tga_minimap)

        for tech_position in self.techs_position:
            x_pos_on_minimap = (tech_position[0] * tga_minimap.width) / self.map_size[0]
            y_pos_on_minimap = (tech_position[1] * tga_minimap.height) / self.map_size[1]

            self._draw_circle(tga_minimap_draw, (x_pos_on_minimap, y_pos_on_minimap), diameter=4, fill='yellow', outline='black')

        for supply_position in self.supplies_position:
            x_pos_on_minimap = (supply_position[0] * tga_minimap.width) / self.map_size[0]
            y_pos_on_minimap = (supply_position[1] * tga_minimap.height) / self.map_size[1]

            self._draw_circle(tga_minimap_draw, (x_pos_on_minimap, y_pos_on_minimap), diameter=4, fill='green', outline='black')

        for player_position in self.players_position:
            x_pos_on_minimap = (player_position[0] * tga_minimap.width) / self.map_size[0]
            y_pos_on_minimap = (player_position[1] * tga_minimap.height) / self.map_size[1]

            self._draw_circle(tga_minimap_draw, (x_pos_on_minimap, y_pos_on_minimap), diameter=8, fill='lightblue', outline='black')

        tga_minimap.save(png_minimap_path, 'PNG')

    def _draw_circle(self, surface, pos, diameter=6, fill=None, outline=None):
        radius = abs(diameter / 2)

        elipse_pos = [
            pos[0] - radius,
            pos[1] - radius,
            pos[0] + radius,
            pos[1] + radius
        ]

        surface.ellipse(elipse_pos, fill=fill, outline=outline)

    def __repr__(self):
        return str(self.__dict__)


class MapCacheParser:
    _maps = []

    def __init__(self, path):
        self._path = path

        if not os.path.isfile(self._path):
            raise FileNotFoundError('The MapCache file was not found: {}'.format(self._path))

    def parse(self):
        with open(self._path, 'r') as f:
            current_map = None
            in_map_section = False

            for line in f:
                line = line.strip()

                if not line or line.startswith(';'): # Empty or comment line
                    continue

                if line.startswith('MapCache'): # MapCache section start
                    file_path = unquote(line.replace('MapCache ', '').replace('_', '%'))

                    current_map = Map()
                    current_map.file_path = file_path
                    current_map.name = os.path.splitext(os.path.basename(file_path))[0].title()

                    in_map_section = True
                elif line == 'END': # MapCache section end
                    self._maps.append(current_map)

                    current_map = None
                    in_map_section = False
                elif in_map_section: # MapCache section key and values
                    param = [s.strip() for s in line.split('=', maxsplit=1)]

                    if len(param) != 2:
                        continue

                    name, value = self._process_param(param)

                    if value is None:
                        continue

                    if name == 'techPosition':
                        current_map.techs_position.append(value)
                    elif name == 'supplyPosition':
                        current_map.supplies_position.append(value)
                    elif name.startswith('Player_') and name.endswith('_Start'):
                        current_map.players_position.append(value)
                    else:
                        setattr(current_map, name, value)

    def search(self, num_players=None):
        results = []

        for map in self._maps:
            num_players_match = False

            if num_players is not None:
                num_players_match = map.num_players == num_players

            if num_players_match:
                results.append(map)

        return results

    def build_minimaps(self):
        for map in self._maps:
            try:
                map.build_minimap()
            except Exception as e:
                logging.error(e)

    def _process_param(self, param):
        name, value = param

        # Value processing
        if name in ['numPlayers', 'fileSize']:
            value = int(value)
        elif name in ['isOfficial', 'isMultiplayer']:
            value = value == 'yes'
        elif name in ['extentMax', 'techPosition', 'supplyPosition'] or (name.startswith('Player_') and name.endswith('_Start')):
            value = self._parse_coords(value)
        else:
            value = None # Invalid values will be removed

        # Name processing
        if name in ['numPlayers', 'fileSize', 'isOfficial', 'isMultiplayer']:
            name = self._snake_case(name)
        elif name == 'extentMax':
            name = 'map_size'

        return name, value

    def _parse_coords(self, coords):
        x, y, z = coords.replace('X:', '').replace('Y:', '').replace('Z:', '').split(' ')

        return (float(x), float(y), float(z))

    def _snake_case(self, name):
        s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)

        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()


class CnCGeneralsAndZeroHourMapsManager(QMainWindow):
    def __init__(self):
        super().__init__()

        self.resize(800, 600)

        self.setWindowTitle('C&C Generals & Zero Hour Maps Manager')

        self.createMapsTable()

        self.center()
        self.show()

    def createMapsTable(self):
        mcp = MapCacheParser('C:\\Users\\Maxime GROSS\\Documents\\Command and Conquer Generals Zero Hour Data\\Maps\\MapCache.ini')

        mcp.parse()
        #mcp.build_minimaps()

        self.maps_table = QTableWidget(len(mcp._maps), 7)
        self.maps_table.setFocusPolicy(Qt.NoFocus)
        self.maps_table.setSelectionBehavior(QTableWidget.SelectRows)
        self.maps_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.maps_table.setHorizontalHeaderLabels([
            'Preview',
            'Name',
            'Official?',
            'Multiplayer?',
            'Players',
            'Capturable buildings',
            'Supply points'
        ])

        maps_table_vertical_header = self.maps_table.verticalHeader()
        maps_table_vertical_header.hide()

        maps_table_horizontal_header = self.maps_table.horizontalHeader()
        maps_table_horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents)
        maps_table_horizontal_header.setStretchLastSection(True)

        i = 0

        for map in mcp._maps:
            png_minimap_path = map.file_path.replace('.map', '.png')

            if os.path.isfile(png_minimap_path):
                minimap_pixmap = QPixmap(png_minimap_path)

                minimap_cell = QTableWidgetItem()
                minimap_cell.setData(Qt.DecorationRole, minimap_pixmap)

                self.maps_table.setItem(i, 0, minimap_cell)
            else:
                self.maps_table.setItem(i, 0, QTableWidgetItem(''))

            self.maps_table.setItem(i, 1, QTableWidgetItem(map.name))
            self.maps_table.setItem(i, 2, QTableWidgetItem('Oui' if map.is_official else 'Non'))
            self.maps_table.setItem(i, 3, QTableWidgetItem('Oui' if map.is_multiplayer else 'Non'))
            self.maps_table.setItem(i, 4, QTableWidgetItem(str(map.num_players)))
            self.maps_table.setItem(i, 5, QTableWidgetItem(str(len(map.techs_position))))
            self.maps_table.setItem(i, 6, QTableWidgetItem(str(len(map.supplies_position))))

            i += 1

        self.maps_table.setSortingEnabled(True)
        self.maps_table.resizeRowsToContents()
        self.setCentralWidget(self.maps_table)

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

if __name__ == '__main__':
    logging.basicConfig(
        format='%(asctime)s - %(levelname)s - %(message)s',
        datefmt='%d/%m/%Y %H:%M:%S',
        stream=sys.stdout
    )

    logging.getLogger().setLevel(logging.INFO)

    app = QApplication(sys.argv)
    cnc_generals_zh_maps_manager = CnCGeneralsAndZeroHourMapsManager()
    sys.exit(app.exec_())