commit 0287dca42fa7e06e2712659eb88a2b6c07dcdb69 Author: Hipstercat Date: Thu Dec 23 00:25:10 2021 +0100 ini diff --git a/lib/constants.py b/lib/constants.py new file mode 100644 index 0000000..9840d5c --- /dev/null +++ b/lib/constants.py @@ -0,0 +1,125 @@ +class AppGUID: + Release = b"\x10\x5e\x62\xa7\x96\x1a\xd2\x11\x9a\xfc\x00\x60\x08\x45\xe5\x71" + Beta = b'j\xfbF/Le\xedJ\x8c\x0b\x06@\x14f\x9b\xfd' + +class MapID: + ID = { + "MvM_L1": 19, + "MvM_L2": 20, + "MvM_L3": 21, + "RvR_L1": 22, + "RvR_L2": 23, + "RvR_L3": 24, + "3W_L1": 25, + "3W_L2": 26, + "3W_L3": 27, + "MvM_LW1": 28, + "RvR_LW1": 29, + "3W_LW1": 30, + "MvM_L6": 31, + } + + @staticmethod + def get_id(name): + if name in MapID.ID: + return MapID.ID[name] + else: + return 0xFF + + @staticmethod + def get_name(map_id): + for _map_name in MapID.ID: + _map_id = MapID.ID[_map_name] + if _map_id == map_id: + return _map_name + return None + +class Teams: + @staticmethod + def get_name_by_id(team_id): + for prop in Teams.__dict__: + if Teams.__dict__[prop] == team_id: + return prop + return "Unknown" + + @staticmethod + def get_groupid_by_team_id(_id, teams): + print("get_groupid_by_team_id: %s %s" % (_id, teams)) + if teams == Teams.MvK: + if _id == 1: + return 2 + if _id == 2: + return 1 + if teams == Teams.MvMvM or teams == Teams.MvM: + return 2 + if teams == Teams.RvR: + return 3 + if teams == Teams.MvR: + if _id == 1: + return 2 + if _id == 2: + return 3 + if teams == Teams.MvRvK: + if _id == 1: + return 2 + if _id == 2: + return 3 + if _id == 3: + return 1 + if teams == Teams.RvK: + if _id == 1: + return 3 + if _id == 2: + return 1 + return 0 + MvM = 0x00 + MvMvM = 0x01 + RvR = 0x02 + MvR = 0x03 + MvRvK = 0x04 + MvK = 0x05 + RvK = 0x06 + TeamB = 0x07 + TeamB = 0x08 + TeamB = 0x0c + TeamB = 0x10 + +class GameTypes: + @staticmethod + def get_name_by_id(gametype_id): + for prop in GameTypes.__dict__: + if GameTypes.__dict__[prop] == gametype_id: + return prop + return "Unknown" + # 00: Team Deathmatch + # 01: Team Deathmatch with full base + # 02: Capture Smartie + # 03: Capture Smartie with full base + # 04: Base Build Deathmatch + # 05: Base Build and Capture the Smartie + # 06: Defend Base + # 07: Defend Base and Capture the Smartie + # 08: GTypeStone + # 09: GTypeWood + # 0a: crash to desktop + # 0b: crash to desktop + # 0c: GType(null) + # 0d: crash to desktop + # 0e: crash to desktop + # 0f: crash to desktop + # 10: crash to desktop + # aa: crash to desktop + # ff: crash to desktop + + TeamDeathmatch = 0x00 + TeamDeathmatchWithFullBase = 0x01 + CaptureSmartie = 0x02 + CaptureSmartieWithFullBase = 0x03 + BaseBuildDeathmatch = 0x04 + BaseBuildCaptureSmartie = 0x05 + DefendBase = 0x06 + DefendBaseCaptureSmartie = 0x07 + GTypeStone = 0x08 + GTypeWood = 0x09 + Crash = 0x0a + GTypeNull = 0x0c diff --git a/lib/enumquery.py b/lib/enumquery.py new file mode 100644 index 0000000..9841f7e --- /dev/null +++ b/lib/enumquery.py @@ -0,0 +1,47 @@ +from lib.packet import Packet +import random + + +class EnumQuery: + LEAD = 0x00 + COMMAND = 0x02 + NO_APPLICATION_GUID = 0x02 + HAS_APPLICATION_GUID = 0x01 + + def __init__(self): + self.Lead = EnumQuery.LEAD + self.Command = EnumQuery.COMMAND + self.Payload = random.getrandbits(16) + self.Type = None + self.ApplicationGUID = None + self.ApplicationPayload = None + + @classmethod + def parse(cls, packet: Packet): + enumquery = cls() + packet.seek(0) + enumquery.Lead = packet.get_byte() + enumquery.Command = packet.get_byte() + enumquery.Payload = packet.get_short() + enumquery.Type = packet.get_byte() + if enumquery.Type == EnumQuery.HAS_APPLICATION_GUID: + enumquery.ApplicationGUID = packet.get_bytes(16) + enumquery.ApplicationPayload = packet.read() + elif enumquery.Type == EnumQuery.NO_APPLICATION_GUID: + enumquery.ApplicationPayload = packet.read() + return enumquery + + def to_packet(self): + packet = Packet() + packet.put_byte(EnumQuery.LEAD) + packet.put_byte(EnumQuery.COMMAND) + packet.put_short(self.Payload) + if self.ApplicationGUID: + packet.put_byte(EnumQuery.HAS_APPLICATION_GUID) + packet.put_bytes(self.ApplicationGUID) + else: + packet.put_byte(EnumQuery.NO_APPLICATION_GUID) + + if self.ApplicationPayload: + packet.put_bytes(self.ApplicationPayload) + return packet diff --git a/lib/enumresponse.py b/lib/enumresponse.py new file mode 100644 index 0000000..89d32e7 --- /dev/null +++ b/lib/enumresponse.py @@ -0,0 +1,117 @@ +from lib.packet import Packet + + +class EnumResponse: + LEAD = 0x00 + COMMAND = 0x03 + + def __init__(self): + self.Lead = EnumResponse.LEAD + self.Command = EnumResponse.COMMAND + self.Payload = b'' + self.ReplyOffset = b'' + self.ReplySize = b'' + self.ApplicationDescSize = b'' + self.ApplicationDescFlags = b'' + self.MaxPlayers = b'' + self.CurrentPlayers = b'' + self.SessionNameOffset = b'' + self.SessionNameSize = b'' + self.PasswordOffset = b'' + self.PasswordSize = b'' + self.ReservedDataOffset = b'' + self.ReservedDataSize = b'' + self.ApplicationReservedDataOffset = b'' + self.ApplicationReservedDataSize = b'' + self.ApplicationInstanceGUID = b'' + self.ApplicationGUID = b'' + self.SessionName = "" + self.Password = b'' + self.ReservedData = b'' + self.ApplicationReservedData = b'' + self.ApplicationData = b'' + + @classmethod + def parse(cls, packet: Packet): + enumresponse = cls() + packet.seek(0) + enumresponse.Lead = packet.get_byte() + if enumresponse.Lead != 0xcf: + enumresponse.Command = packet.get_byte() + enumresponse.Payload = packet.get_short() + enumresponse.ReplyOffset = packet.get_ulong() + enumresponse.ReplySize = packet.get_ulong() + else: + packet.get_3bytes_int() # cause we were supposed to read a full int + + enumresponse.ApplicationDescSize = packet.get_ulong() + enumresponse.ApplicationDescFlags = packet.get_ulong() + enumresponse.MaxPlayers = packet.get_ulong() + enumresponse.CurrentPlayers = packet.get_ulong() + enumresponse.SessionNameOffset = packet.get_ulong() + enumresponse.SessionNameSize = packet.get_ulong() + enumresponse.PasswordOffset = packet.get_ulong() + enumresponse.PasswordSize = packet.get_ulong() + enumresponse.ReservedDataOffset = packet.get_ulong() + enumresponse.ReservedDataSize = packet.get_ulong() + enumresponse.ApplicationReservedDataOffset = packet.get_ulong() + enumresponse.ApplicationReservedDataSize = packet.get_ulong() + enumresponse.ApplicationInstanceGUID = packet.get_bytes(16) + enumresponse.ApplicationGUID = packet.get_bytes(16) + enumresponse.SessionName = packet.getvalue()[enumresponse.SessionNameOffset+4:enumresponse.SessionNameOffset+enumresponse.SessionNameSize+1].decode("utf8").replace("\x00", "") + enumresponse.ApplicationReservedData = packet.getvalue()[enumresponse.ApplicationReservedDataOffset+4:enumresponse.ApplicationReservedDataOffset+enumresponse.ApplicationReservedDataSize+4] + return enumresponse + + def to_packet(self): + varpos = 88 + self.SessionNameSize = len(self.SessionName.encode("utf-16")) + self.SessionNameOffset = varpos if self.SessionName else 0 + varpos += self.SessionNameSize if self.SessionName else 0 + + self.PasswordSize = len(self.Password) + self.PasswordOffset = varpos if self.Password else 0 + varpos += self.PasswordSize if self.Password else 0 + + self.ReservedDataSize = len(self.ReservedData) + self.ReservedDataOffset = varpos if self.ReservedData else 0 + varpos += self.ReservedDataSize if self.ReservedData else 0 + + self.ApplicationReservedDataSize = len(self.ApplicationReservedData) + self.ApplicationReservedDataOffset = varpos if self.ApplicationReservedData else 0 + varpos += self.ApplicationReservedDataSize if self.ApplicationReservedData else 0 + + self.ReplySize = len(self.ApplicationData) + self.ReplyOffset = varpos if self.ApplicationData else 0 + varpos += self.ReplySize if self.ApplicationData else 0 + + packet = Packet() + packet.put_byte(EnumResponse.LEAD) + packet.put_byte(EnumResponse.COMMAND) + packet.put_short(self.Payload) + packet.put_ulong(self.ReplyOffset) + packet.put_ulong(self.ReplySize) + packet.put_ulong(self.ApplicationDescSize) + packet.put_ulong(self.ApplicationDescFlags) + packet.put_ulong(self.MaxPlayers) + packet.put_ulong(self.CurrentPlayers) + packet.put_ulong(self.SessionNameOffset) + packet.put_ulong(self.SessionNameSize) + packet.put_ulong(self.PasswordOffset) + packet.put_ulong(self.PasswordSize) + packet.put_ulong(self.ReservedDataOffset) + packet.put_ulong(self.ReservedDataSize) + packet.put_ulong(self.ApplicationReservedDataOffset) + packet.put_ulong(self.ApplicationReservedDataSize) + packet.put_bytes(self.ApplicationInstanceGUID) + packet.put_bytes(self.ApplicationGUID) + if self.SessionName: + packet.put_bytes((self.SessionName+'\x00').encode("utf-16-le")) + if self.Password: + packet.put_bytes(self.Password) + if self.ReservedData: + packet.put_bytes(self.ReservedData) + if self.ApplicationReservedData: + packet.put_bytes(self.ApplicationReservedData) + if self.ApplicationData: + packet.put_bytes(self.ApplicationData) + return packet diff --git a/lib/giantsenumresponseparser.py b/lib/giantsenumresponseparser.py new file mode 100644 index 0000000..11a4971 --- /dev/null +++ b/lib/giantsenumresponseparser.py @@ -0,0 +1,89 @@ +from lib.enumresponse import EnumResponse, Packet +from lib.constants import Teams, GameTypes, MapID + + +class GiantsEnumResponseParser: + def __init__(self): + super().__init__() + self.server_name = None + + self.max_players = None + self.current_players = None + + self.game_type = None + self.teams = None + self.version = None + self.points_per_capture = None + self.points_per_kill = None + self.base_level = None + + self.allow_joiners = None + self.damage_teammates = None + self.lock_teams = None + self.is_dedicated = None + self.vimps_disabled = None + self.weap_bits = None + self.high_bandwidth = None + self.no_voting = None + self.smartie_difficulty = None + self.vimp_difficulty = None + + self.capture_prevent_count = None + self.current_cap_prev_time = None + + self.map_id = None + self.map_checksum = None + self.map_name = None + self.build = None + self.revision = None + + @classmethod + def parse(cls, enumresp: EnumResponse): + g_enumresponse = cls() + g_enumresponse.max_players = enumresp.MaxPlayers + g_enumresponse.current_players = enumresp.CurrentPlayers + g_enumresponse.server_name = enumresp.SessionName + + game_data = enumresp.ApplicationReservedData + # print(game_data) + p = Packet(game_data) + g_enumresponse.map_id = p.get_byte() + g_enumresponse.game_type = p.get_byte() + g_enumresponse.teams = p.get_byte() + g_enumresponse.base_level = p.get_byte() + g_enumresponse.version = p.get_short() + + bitmask = p.get_short() + b = bin(bitmask)[2:].zfill(16) + g_enumresponse.allow_joiners = b[0] + g_enumresponse.damage_teammates = b[1] + g_enumresponse.lock_teams = b[2] + g_enumresponse.is_dedicated = b[3] + g_enumresponse.vimps_disabled = b[4] + g_enumresponse.weap_bits = b[5] + g_enumresponse.high_bandwidth = b[6] + g_enumresponse.no_voting = b[7] + + g_enumresponse.smartie_difficulty = b[8:12] + g_enumresponse.vimp_difficulty = b[12:16] + + g_enumresponse.points_per_capture = p.get_short() + g_enumresponse.points_per_kill = p.get_short() + g_enumresponse.capture_prevent_count = p.get_short() + g_enumresponse.current_cap_prev_time = p.get_short() + + g_enumresponse.map_checksum = p.get_ulong() + g_enumresponse.map_name = p.get_string(32) + + if g_enumresponse.version >= 1498: + g_enumresponse.build = p.get_byte() + g_enumresponse.revision = p.get_byte() + + if map_name := MapID.get_name(g_enumresponse.map_id): + g_enumresponse.map_name = map_name + + return g_enumresponse + + def __str__(self): + return "[%s] Players:%d/%d, Map: %s, Type: %s, Teams: %s, Version: %d, Build: %s, AllowJoiners: %s" % \ + (self.server_name, self.current_players, self.max_players, self.map_name, GameTypes.get_name_by_id(self.game_type), Teams.get_name_by_id(self.teams), self.version, self.build, self.allow_joiners) diff --git a/lib/packet.py b/lib/packet.py new file mode 100644 index 0000000..06334da --- /dev/null +++ b/lib/packet.py @@ -0,0 +1,70 @@ +import io +import struct + + +class Packet(io.BytesIO): + def put_byte(self, val) -> None: + self.write(struct.pack(' int: + return struct.unpack(' None: + for byte in b: + self.write(struct.pack(' bytes: + return bytes(struct.unpack('<'+str(num)+'B', self.read(num))) + + def put_short(self, val) -> None: + self.write(struct.pack(' int: + return struct.unpack(' None: + self.write(struct.pack(' None: + self.write(struct.pack(' int: + return struct.unpack(' int: + return struct.unpack(' None: + self.write(struct.pack(' int: + return struct.unpack(' None: + self.write(struct.pack(' float: + return struct.unpack(' None: + self.write(val + b'\x00') + + def put_string_size(self, val, size) -> None: + self.write(val.encode("utf8") + b"\x00" * (size - len(val))) + + def get_string_until_none(self) -> str: + s = "" + c = self.get_byte() + while c != 0x00: + s += chr(c) + c = self.get_byte() + return s + + def get_string(self, size) -> str: + return self.get_bytes(size).decode("utf8").replace("\x00", "") + + def get_3bytes_int(self) -> int: + return struct.unpack(" None: + self.write(struct.pack(" None: + full_msg = f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}" + self.ui.logEdit.appendPlainText(full_msg) + print(full_msg) + + +def get_maps_dir() -> pathlib.Path: + return get_giants_directory() / "Bin" / "Worlds" + + +def load_installed_maps() -> None: + maps_dir = get_maps_dir() + + def map_crc(map_file): + prev = 0 + with open(map_file, "rb") as f: + b = f.read() + prev = zlib.crc32(b, prev) + return prev & 0xffffffff + + installed_maps.clear() + for map_path in os.listdir(maps_dir): + if map_path.endswith(".cache") or map_path.endswith(".gck"): + map_path_s = pathlib.Path(maps_dir / map_path) + crc = map_crc(map_path_s) + installed_maps[crc] = map_path_s + + +def config_file_path() -> pathlib.Path: + datadir = get_datadir() / "giants-maps-downloader" + config_file = datadir / "config.yml" + try: + datadir.mkdir(parents=True) + except FileExistsError: + pass + return config_file + + +def read_configuration(): + config_file = config_file_path() + global CONFIG + if not os.path.exists(config_file): + # create it + giants_exe_path, _filename = QFileDialog.getOpenFileName(None, caption="Open Giants.exe", filter="Giants.exe") + giants_dir = os.path.dirname(giants_exe_path) + CONFIG = {"giants_directory": giants_dir, "automatic_download": True} + save_configuration() + + print(f"Config file: {config_file}") + with open(config_file, "r") as fp: + CONFIG = yaml.load(fp, Loader=yaml.Loader) + + +def save_configuration(): + config_file = config_file_path() + with open(config_file, "w") as fp: + yaml.dump(CONFIG, fp) + print(f"Wrote configuration file to {config_file}") + + +def get_giants_directory() -> pathlib.Path: + return pathlib.Path(CONFIG["giants_directory"]) + + +def get_datadir() -> pathlib.Path: + home = pathlib.Path.home() + if sys.platform == "win32": + return home / "AppData/Roaming" + elif sys.platform == "linux": + return home / ".local/share" + elif sys.platform == "darwin": + return home / "Library/Application Support" + + +class MapsBackground(QThread): + msg = Signal(str) + + def run(self) -> None: + self.log("Background task starting") + try: + self.server_up_or_dl_missing_map(LOCAL_IP, 19711, True, timeout=2) + except: + self.log("No local server found") + self.all_servers_download() + + def log(self, message: str): + self.msg.emit(message) + + def server_up_or_dl_missing_map(self, server_ip: str, server_port: int, is_local: bool, timeout=5) -> None: + enum_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + enum_socket.setblocking(True) + enum_socket.settimeout(timeout) + + eq = EnumQuery() + eq.Type = EnumQuery.HAS_APPLICATION_GUID + eq.ApplicationGUID = AppGUID.Release + p = eq.to_packet().getvalue() + enum_socket.sendto(p, (server_ip, server_port)) + + response = enum_socket.recv(1024) + enumresp = EnumResponse.parse(Packet(response)) + giantsenumresp = GiantsEnumResponseParser.parse(enumresp) + if is_local: + self.upload_map(installed_maps[giantsenumresp.map_checksum]) + elif giantsenumresp.map_checksum not in installed_maps: + self.log(f"Map {giantsenumresp.map_name} not found locally, downloading it") + self.download_map(giantsenumresp.map_checksum) + + def download_map(self, crc: int) -> None: + self.log(f"Downloading map {API_BASE_URL}/map?crc={crc}") + try: + req = requests.get(f"{API_BASE_URL}/map?crc={crc}") + except Exception as e: + self.log(f"Error while fetching map info: {e}") + return + if req.status_code != 200: + self.log(f"Could not download map from server, got status_code {req.status_code}") + return + j = req.json() + blob_location = j["blob_location"] + + final_map_file = get_maps_dir() / j["name"] + + with requests.get(blob_location, stream=True) as stream: + with open(final_map_file, 'wb') as f: + for chunk in stream.iter_content(chunk_size=8192): + f.write(chunk) + + load_installed_maps() + self.log(f"Download succesful: {final_map_file}") + + def upload_map(self, map_path: pathlib.Path) -> None: + with open(map_path, "rb") as fp: + content_bytes = fp.read() + + def crc(map_content_bytes): + prev = zlib.crc32(map_content_bytes, 0) + return prev & 0xffffffff + + map_crc = crc(content_bytes) + + # check if map is known by server + self.log(f"Trying {API_BASE_URL}/map?crc={map_crc}") + try: + req = requests.get(f"{API_BASE_URL}/map?crc={map_crc}") + except Exception as e: + self.log(f"Error while fetching map info: {e}") + return + if req.status_code == 200: + self.log(f"API already knows this map, got response {req.status_code}") + return + + map_name = os.path.basename(map_path) + self.log(f"Local server found hosting map {map_name}, uploading it") + b64_data = base64.b64encode(content_bytes).decode("utf8") + map_data = {"name": map_name, "b64_data": b64_data} + try: + req = requests.post(f"{API_BASE_URL}/maps", json=map_data) + req.raise_for_status() + except Exception as e: + self.log(f"There was an error while uploading map: {e}") + return + self.log("Upload successful") + + def all_servers_download(self) -> None: + all_servers = requests.get("https://giants.azurewebsites.net/api/Servers").json() + for server in all_servers: + try: + self.server_up_or_dl_missing_map(server["hostIpAddress"], server["port"], False) + except socket.timeout: + self.log(f"Could not connect to {server['hostIpAddress']}") + + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(True) + read_configuration() + load_installed_maps() + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..3466b78 --- /dev/null +++ b/main.spec @@ -0,0 +1,40 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis(['main.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['PySide6.QtQml'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='main', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None ) diff --git a/maps_gui.py b/maps_gui.py new file mode 100644 index 0000000..5c56f59 --- /dev/null +++ b/maps_gui.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'maps_gui.ui' +## +## Created by: Qt User Interface Compiler version 6.2.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QCheckBox, QFormLayout, QGridLayout, + QLabel, QMainWindow, QMenuBar, QPlainTextEdit, + QPushButton, QSizePolicy, QStatusBar, QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(449, 230) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.centralwidget.setEnabled(True) + self.gridLayout_2 = QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout = QGridLayout() + self.gridLayout.setObjectName(u"gridLayout") + self.formLayout = QFormLayout() + self.formLayout.setObjectName(u"formLayout") + self.automaticMapDownloadLabel = QLabel(self.centralwidget) + self.automaticMapDownloadLabel.setObjectName(u"automaticMapDownloadLabel") + + self.formLayout.setWidget(0, QFormLayout.LabelRole, self.automaticMapDownloadLabel) + + self.automaticMapDownloadCheckBox = QCheckBox(self.centralwidget) + self.automaticMapDownloadCheckBox.setObjectName(u"automaticMapDownloadCheckBox") + + self.formLayout.setWidget(0, QFormLayout.FieldRole, self.automaticMapDownloadCheckBox) + + + self.gridLayout.addLayout(self.formLayout, 0, 0, 1, 1) + + self.pushButton = QPushButton(self.centralwidget) + self.pushButton.setObjectName(u"pushButton") + self.pushButton.setEnabled(True) + + self.gridLayout.addWidget(self.pushButton, 0, 1, 1, 1) + + self.logEdit = QPlainTextEdit(self.centralwidget) + self.logEdit.setObjectName(u"logEdit") + self.logEdit.setReadOnly(True) + + self.gridLayout.addWidget(self.logEdit, 1, 0, 1, 2) + + + self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1) + + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(MainWindow) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 449, 21)) + MainWindow.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"GMD - Giants Maps Downloader", None)) + self.automaticMapDownloadLabel.setText(QCoreApplication.translate("MainWindow", u"Automatic map download", None)) +#if QT_CONFIG(statustip) + self.automaticMapDownloadCheckBox.setStatusTip(QCoreApplication.translate("MainWindow", u"Every few minutes, all servers will be queried and missing maps will be downloaded", None)) +#endif // QT_CONFIG(statustip) +#if QT_CONFIG(statustip) + self.pushButton.setStatusTip(QCoreApplication.translate("MainWindow", u"Immediately query servers now, download missing maps and upload hosted ones", None)) +#endif // QT_CONFIG(statustip) + self.pushButton.setText(QCoreApplication.translate("MainWindow", u"Download currently missing maps\n" +"hosted by servers", None)) +#if QT_CONFIG(statustip) + self.logEdit.setStatusTip(QCoreApplication.translate("MainWindow", u"Log messages", None)) +#endif // QT_CONFIG(statustip) +#if QT_CONFIG(statustip) + self.statusbar.setStatusTip(QCoreApplication.translate("MainWindow", u"By Amazed#0001", None)) +#endif // QT_CONFIG(statustip) + # retranslateUi + diff --git a/maps_gui.ui b/maps_gui.ui new file mode 100644 index 0000000..afb126d --- /dev/null +++ b/maps_gui.ui @@ -0,0 +1,87 @@ + + + MainWindow + + + + 0 + 0 + 449 + 230 + + + + GMD - Giants Maps Downloader + + + + true + + + + + + + + + + Automatic map download + + + + + + + Every few minutes, all servers will be queried and missing maps will be downloaded + + + + + + + + + true + + + Immediately query servers now, download missing maps and upload hosted ones + + + Download currently missing maps +hosted by servers + + + + + + + Log messages + + + true + + + + + + + + + + + 0 + 0 + 449 + 21 + + + + + + By Amazed#0001 + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cce72a9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +PySide6 +requests +pyyaml +pyinstaller \ No newline at end of file