init
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
# Universal Game Updater
|
||||||
|
|
||||||
|
A simple, self-hosted updater for distributing game client files. Consists of three parts:
|
||||||
|
|
||||||
|
- **`updater.py`** — the client-side app (compiled to `updater.exe`) that players run. Checks the server for changed files and downloads only what's needed.
|
||||||
|
- **`server.py`** — a lightweight HTTP server that serves client files and manifests.
|
||||||
|
- **`generate_manifest.py`** — a script you run on your build machine to generate the manifest the server distributes.
|
||||||
|
- **`generate_key.py`** — a helper to generate access keys (see [Access keys](#access-keys) below).
|
||||||
|
|
||||||
|
Works with any game — just fill in the configuration blocks at the top of each script.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. You put your game's files in a folder on the server (e.g. `game_client/`).
|
||||||
|
2. You run `generate_manifest.py` on that folder. It walks the folder, hashes every file (MD5), and writes a `manifest.json` listing `relative/path: hash` for every file.
|
||||||
|
3. `server.py` serves that folder over HTTP, keyed by an access key defined in `config.json`.
|
||||||
|
4. Players run `updater.exe`. It downloads the server's `manifest.json`, compares hashes against their local files, and downloads anything missing or different.
|
||||||
|
5. Once everything matches, the "Play" button launches the game executable.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Server
|
||||||
|
|
||||||
|
Install Python 3 (no external dependencies needed — everything uses the standard library).
|
||||||
|
|
||||||
|
Generate an access key for each client version you want to distribute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python generate_key.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints a random 64-character hex string. Generate one per client (e.g. one for `main`, one for `test`).
|
||||||
|
|
||||||
|
Edit `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 3232,
|
||||||
|
"clients": {
|
||||||
|
"<your-generated-hex-key>": "game_client",
|
||||||
|
"<another-generated-hex-key>": "game_client_test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `host` / `port` — what the server listens on.
|
||||||
|
- `clients` — maps an access key to the folder (relative to `server.py`) that key serves.
|
||||||
|
|
||||||
|
Put your game files in the folders referenced above (`game_client/`, etc.), then generate the manifest for each:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python generate_manifest.py game_client
|
||||||
|
python generate_manifest.py game_client_test
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run this after every update to the client files, then start (or restart) the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Files become available at `http://<server-address>:<port>/<access-key>/...`.
|
||||||
|
|
||||||
|
### 2. Updater (client)
|
||||||
|
|
||||||
|
Edit the configuration block at the top of `updater.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
GAME_NAME = "My Game"
|
||||||
|
SERVER_ADDRESS = "update.mygame.com" # domain or IP
|
||||||
|
SERVER_PORT = "3232"
|
||||||
|
USE_HTTPS = False
|
||||||
|
|
||||||
|
CLIENT_KEYS = {
|
||||||
|
"Main": "<same-hex-key-as-in-server-config.json>",
|
||||||
|
"Test": "<same-hex-key-as-in-server-config.json>",
|
||||||
|
}
|
||||||
|
DEFAULT_CLIENT = "Main"
|
||||||
|
|
||||||
|
GAME_EXE_RELATIVE_PATH = os.path.join("system", "game.exe")
|
||||||
|
BANNER_RELATIVE_PATH = "banner.png"
|
||||||
|
ICON_RELATIVE_PATH = "icon.ico"
|
||||||
|
```
|
||||||
|
|
||||||
|
- `SERVER_ADDRESS` accepts either a domain name or a raw IP — nothing else needs to change.
|
||||||
|
- `CLIENT_KEYS` maps a friendly name shown in the UI to the actual access key sent in the request URL. Keys must match `config.json` on the server exactly.
|
||||||
|
- `GAME_EXE_RELATIVE_PATH` is where the updater looks for the game's executable, relative to the folder the updater itself is placed in.
|
||||||
|
|
||||||
|
Then compile with your favorite tool, e.g. [PyInstaller](https://pyinstaller.org/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyinstaller --onefile --noconsole --icon=icon.ico --add-data "banner.png;." --add-data "icon.ico;." updater.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ship the resulting `updater.exe` alongside the game folder.
|
||||||
|
|
||||||
|
## Access keys
|
||||||
|
|
||||||
|
Client folders are addressed by a long random hex key instead of a plain name like `main`, so the update path isn't obvious or easy to stumble on by guessing.
|
||||||
|
|
||||||
|
**This is obscurity, not real security.** The key has to live inside `updater.exe` for it to work, and anyone motivated enough to disassemble the executable or inspect network traffic can recover it. Don't rely on it to gate anything sensitive.
|
||||||
|
|
||||||
|
If a key leaks and you want to rotate it:
|
||||||
|
|
||||||
|
1. Generate a new key with `generate_key.py`.
|
||||||
|
2. Update it in the server's `config.json`.
|
||||||
|
3. Update it in `updater.py`'s `CLIENT_KEYS`.
|
||||||
|
4. Rebuild and redistribute `updater.exe`.
|
||||||
|
|
||||||
|
No folder renaming or manifest regeneration needed.
|
||||||
|
|
||||||
|
## Ignoring files during manifest generation
|
||||||
|
|
||||||
|
Edit the configuration block at the top of `generate_manifest.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
IGNORE_FILES = {
|
||||||
|
MANIFEST_FILENAME,
|
||||||
|
"updater.exe",
|
||||||
|
"Option.ini",
|
||||||
|
"WindowsInfo.ini",
|
||||||
|
}
|
||||||
|
|
||||||
|
IGNORE_FOLDERS = {
|
||||||
|
"screenshots",
|
||||||
|
"logs",
|
||||||
|
"replays",
|
||||||
|
}
|
||||||
|
|
||||||
|
IGNORE_EXTENSIONS = {
|
||||||
|
".log",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add any player-specific files (settings, saves, screenshots, logs) here so the updater never overwrites or deletes them.
|
||||||
|
|
||||||
|
## Notes / limitations
|
||||||
|
|
||||||
|
- File integrity is checked with MD5 — fine for detecting changed files, not intended as a cryptographic guarantee.
|
||||||
|
- The server has no built-in rate limiting or HTTPS; put it behind a reverse proxy (nginx, Caddy) if you need either.
|
||||||
|
- `updater.py` uses Tkinter for the UI, which ships with most standard Python installs on Windows.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 3232,
|
||||||
|
"clients": {
|
||||||
|
"d5af3f9fd06b15fcfcd1e6204b10804edbb9936b094ece123ce38ee7d3382ee0": "game_client",
|
||||||
|
"aa697a8988d49f161c9764c3bb54b9111ad016bca7470243ea3aa2ee4fba37c1": "game_client_test"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import secrets
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# Generates a long random hex key suitable for use as a client access key.
|
||||||
|
# Run this once per client (main/test/etc.) and put the result into:
|
||||||
|
# - server-side config.json (as the dict key, mapped to the folder)
|
||||||
|
# - updater.py CLIENT_KEYS (as the value, mapped to the display name)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Generate a random hex access key for a client.")
|
||||||
|
parser.add_argument("--bytes", type=int, default=32,
|
||||||
|
help="Number of random bytes (default 32 -> 64 hex chars).")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
key = secrets.token_hex(args.bytes)
|
||||||
|
print(key)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONFIGURATION - edit these values for your game/project
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
MANIFEST_FILENAME = "manifest.json"
|
||||||
|
|
||||||
|
# Files the updater must never touch (launcher itself, local settings, etc.)
|
||||||
|
IGNORE_FILES = {
|
||||||
|
MANIFEST_FILENAME,
|
||||||
|
"updater.exe", # Your compiled updater executable
|
||||||
|
"Option.ini", # Example: graphics/sound settings
|
||||||
|
"WindowsInfo.ini", # Example: window layout settings
|
||||||
|
}
|
||||||
|
|
||||||
|
# Folders the updater must never scan or update
|
||||||
|
IGNORE_FOLDERS = {
|
||||||
|
"screenshots",
|
||||||
|
"logs",
|
||||||
|
"replays",
|
||||||
|
}
|
||||||
|
|
||||||
|
# File extensions to always skip (lowercase, without the dot doesn't matter, include the dot)
|
||||||
|
IGNORE_EXTENSIONS = {
|
||||||
|
".log",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# END CONFIGURATION
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_md5(filepath):
|
||||||
|
"""Computes the MD5 hash of a file."""
|
||||||
|
hash_md5 = hashlib.md5()
|
||||||
|
try:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
|
hash_md5.update(chunk)
|
||||||
|
return hash_md5.hexdigest()
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def generate(client_dir):
|
||||||
|
if not os.path.exists(client_dir) or not os.path.isdir(client_dir):
|
||||||
|
print(f"Error: directory '{client_dir}' not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
manifest = {}
|
||||||
|
manifest_path = os.path.join(client_dir, MANIFEST_FILENAME)
|
||||||
|
|
||||||
|
print(f"Scanning client folder: {client_dir}")
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(client_dir):
|
||||||
|
# Filter out ignored folders (case-insensitive)
|
||||||
|
dirs[:] = [d for d in dirs if d.lower() not in IGNORE_FOLDERS]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if file in IGNORE_FILES:
|
||||||
|
continue
|
||||||
|
if any(file.lower().endswith(ext) for ext in IGNORE_EXTENSIONS):
|
||||||
|
continue
|
||||||
|
|
||||||
|
filepath = os.path.join(root, file)
|
||||||
|
|
||||||
|
# Path relative to the client folder (this is what the updater expects)
|
||||||
|
rel_path = os.path.relpath(filepath, client_dir)
|
||||||
|
|
||||||
|
# Convert Windows-style separators to web-style
|
||||||
|
rel_path_web = rel_path.replace("\\", "/")
|
||||||
|
|
||||||
|
file_hash = get_md5(filepath)
|
||||||
|
if file_hash:
|
||||||
|
manifest[rel_path_web] = file_hash
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(manifest_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(manifest, f, indent=4)
|
||||||
|
print(f"Done! Manifest saved to: {manifest_path}")
|
||||||
|
print(f"Total files in manifest: {len(manifest)}")
|
||||||
|
except IOError as e:
|
||||||
|
print(f"Error saving manifest: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Universal manifest generator for game updaters.")
|
||||||
|
parser.add_argument("client_path", help="Path to the client files folder")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
generate(args.client_path)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import posixpath
|
||||||
|
import urllib.parse
|
||||||
|
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
|
CONFIG_FILENAME = "config.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_FILENAME)
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG = load_config()
|
||||||
|
HOST = CONFIG.get("host", "0.0.0.0")
|
||||||
|
PORT = CONFIG.get("port", 3232)
|
||||||
|
|
||||||
|
# Maps a URL access key (long hex string, see generate_key.py) -> physical
|
||||||
|
# folder on the server holding that client's files. Using a random hex key
|
||||||
|
# instead of a plain name like "main" just makes the files harder to stumble
|
||||||
|
# upon by guessing; it does not stop someone who deliberately extracts the
|
||||||
|
# key from the updater executable. If a key ever leaks, generate a new one
|
||||||
|
# and update it here plus in the updater's CLIENT_KEYS - no need to touch
|
||||||
|
# folder names or anything else.
|
||||||
|
CLIENTS = CONFIG.get("clients", {})
|
||||||
|
|
||||||
|
|
||||||
|
class MultiClientHandler(SimpleHTTPRequestHandler):
|
||||||
|
def translate_path(self, path):
|
||||||
|
path = urllib.parse.urlparse(path).path
|
||||||
|
parts = [p for p in path.split('/') if p]
|
||||||
|
if len(parts) >= 1 and parts[0] in CLIENTS:
|
||||||
|
client_key = parts[0]
|
||||||
|
base_dir = CLIENTS[client_key]
|
||||||
|
rel_path = posixpath.join(*parts[1:]) if len(parts) > 1 else ""
|
||||||
|
return os.path.join(base_dir, rel_path)
|
||||||
|
return super().translate_path(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
server_address = (HOST, PORT)
|
||||||
|
httpd = HTTPServer(server_address, MultiClientHandler)
|
||||||
|
print(f"Server running on {HOST}:{PORT}...")
|
||||||
|
print(f"Available client keys: {', '.join(CLIENTS.keys()) if CLIENTS else '(none configured)'}")
|
||||||
|
httpd.serve_forever()
|
||||||
+221
@@ -0,0 +1,221 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import subprocess
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONFIGURATION - edit these values for your game/project
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
GAME_NAME = "My Game" # Shown in the window title
|
||||||
|
WINDOW_WIDTH = 450
|
||||||
|
WINDOW_HEIGHT = 280
|
||||||
|
|
||||||
|
# Server address. Use either a domain or an IP - whichever you set SERVER_ADDRESS to.
|
||||||
|
# Examples: "update.mygame.com" or "123.456.78.90"
|
||||||
|
SERVER_ADDRESS = "update.mygame.com"
|
||||||
|
SERVER_PORT = "3232"
|
||||||
|
USE_HTTPS = False # Set True if your server is behind HTTPS/SSL
|
||||||
|
|
||||||
|
# Available client versions: display name -> access key.
|
||||||
|
# The access key is a long random hex string (generate with generate_key.py)
|
||||||
|
# and must match a key in the server's config.json "clients" dict.
|
||||||
|
# This just makes the update folder harder to stumble upon by guessing a
|
||||||
|
# short name in the URL. It is NOT real security: anyone willing to
|
||||||
|
# disassemble/decompile this exe or sniff the network traffic can recover
|
||||||
|
# the key, since the program has to know it to work. If a key ever leaks,
|
||||||
|
# generate a new one, update it here and in the server's config.json, and
|
||||||
|
# rebuild the exe - no need to rename anything else.
|
||||||
|
CLIENT_KEYS = {
|
||||||
|
"Main": "d5af3f9fd06b15fcfcd1e6204b10804edbb9936b094ece123ce38ee7d3382ee0",
|
||||||
|
"Test": "aa697a8988d49f161c9764c3bb54b9111ad016bca7470243ea3aa2ee4fba37c1",
|
||||||
|
}
|
||||||
|
DEFAULT_CLIENT = "Main"
|
||||||
|
|
||||||
|
# Relative path (from the game folder) to the executable that launches the game
|
||||||
|
GAME_EXE_RELATIVE_PATH = os.path.join("system", "game.exe")
|
||||||
|
|
||||||
|
# Resource files bundled with the updater (banner/icon), relative to the script/exe
|
||||||
|
BANNER_RELATIVE_PATH = "banner.png"
|
||||||
|
ICON_RELATIVE_PATH = "icon.ico"
|
||||||
|
|
||||||
|
MANIFEST_FILENAME = "manifest.json"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# END CONFIGURATION
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_path(relative_path):
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
return os.path.join(sys._MEIPASS, relative_path)
|
||||||
|
return os.path.join(os.path.abspath("."), relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_path():
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
return os.path.dirname(sys.executable)
|
||||||
|
return os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
BASE_PATH = get_base_path()
|
||||||
|
BANNER_FILE = get_resource_path(BANNER_RELATIVE_PATH)
|
||||||
|
ICON_FILE = get_resource_path(ICON_RELATIVE_PATH)
|
||||||
|
GAME_EXE = os.path.join(BASE_PATH, GAME_EXE_RELATIVE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def build_server_base_url():
|
||||||
|
scheme = "https" if USE_HTTPS else "http"
|
||||||
|
return f"{scheme}://{SERVER_ADDRESS}:{SERVER_PORT}/"
|
||||||
|
|
||||||
|
|
||||||
|
class UpdaterApp:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.root.title(f"{GAME_NAME} Updater")
|
||||||
|
self.root.geometry(f"{WINDOW_WIDTH}x{WINDOW_HEIGHT}")
|
||||||
|
self.root.resizable(False, False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.root.iconbitmap(ICON_FILE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.bg_image = tk.PhotoImage(file=BANNER_FILE)
|
||||||
|
tk.Label(root, image=self.bg_image).pack(pady=5)
|
||||||
|
except Exception:
|
||||||
|
tk.Label(root, text="[ Banner not found ]", bg="gray", width=60, height=6).pack(pady=5)
|
||||||
|
|
||||||
|
# Client version selector
|
||||||
|
mode_frame = tk.Frame(root)
|
||||||
|
mode_frame.pack(pady=5)
|
||||||
|
|
||||||
|
tk.Label(mode_frame, text="Client version:").pack(side="left")
|
||||||
|
|
||||||
|
self.mode_var = tk.StringVar(value=DEFAULT_CLIENT)
|
||||||
|
self.mode_combo = ttk.Combobox(
|
||||||
|
mode_frame, textvariable=self.mode_var,
|
||||||
|
values=list(CLIENT_KEYS.keys()), state="readonly", width=15
|
||||||
|
)
|
||||||
|
self.mode_combo.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# Status text
|
||||||
|
self.status_var = tk.StringVar(value="Select a version and click Check for Updates...")
|
||||||
|
tk.Label(root, textvariable=self.status_var).pack(pady=5)
|
||||||
|
|
||||||
|
# Progress bar
|
||||||
|
self.progress = ttk.Progressbar(root, orient="horizontal", length=400, mode="determinate")
|
||||||
|
self.progress.pack(pady=5)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
btn_frame = tk.Frame(root)
|
||||||
|
btn_frame.pack(pady=10)
|
||||||
|
|
||||||
|
self.btn_update = tk.Button(btn_frame, text="Check for Updates", command=self.start_update, width=20)
|
||||||
|
self.btn_update.pack(side="left", padx=10)
|
||||||
|
|
||||||
|
btn_state = "normal" if os.path.exists(GAME_EXE) else "disabled"
|
||||||
|
self.btn_play = tk.Button(btn_frame, text="Play", command=self.play_game, width=20, state=btn_state)
|
||||||
|
self.btn_play.pack(side="left", padx=10)
|
||||||
|
|
||||||
|
def update_ui(self, text=None, prog_val=None, prog_max=None):
|
||||||
|
if text: self.root.after(0, lambda: self.status_var.set(text))
|
||||||
|
if prog_max: self.root.after(0, lambda: self.progress.config(maximum=prog_max))
|
||||||
|
if prog_val is not None: self.root.after(0, lambda: self.progress.config(value=prog_val))
|
||||||
|
|
||||||
|
def get_md5(self, filepath):
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return None
|
||||||
|
hash_md5 = hashlib.md5()
|
||||||
|
try:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
|
hash_md5.update(chunk)
|
||||||
|
return hash_md5.hexdigest()
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def start_update(self):
|
||||||
|
self.btn_update.config(state="disabled")
|
||||||
|
self.btn_play.config(state="disabled")
|
||||||
|
self.mode_combo.config(state="disabled") # Lock the selector during update
|
||||||
|
threading.Thread(target=self.update_process, daemon=True).start()
|
||||||
|
|
||||||
|
def update_process(self):
|
||||||
|
# Build URLs dynamically based on the selected client version.
|
||||||
|
# The display name is only for the UI - the actual URL uses the
|
||||||
|
# hex access key so the folder name isn't guessable from the UI text.
|
||||||
|
display_name = self.mode_var.get()
|
||||||
|
access_key = CLIENT_KEYS[display_name]
|
||||||
|
current_server_url = build_server_base_url() + f"{access_key}/"
|
||||||
|
manifest_url = current_server_url + MANIFEST_FILENAME
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.update_ui(text=f"Connecting to server ({display_name})...")
|
||||||
|
req = urllib.request.urlopen(manifest_url, timeout=10)
|
||||||
|
server_manifest = json.loads(req.read().decode('utf-8'))
|
||||||
|
|
||||||
|
files_to_download = []
|
||||||
|
total_files = len(server_manifest)
|
||||||
|
|
||||||
|
self.update_ui(prog_max=total_files, prog_val=0)
|
||||||
|
for i, (rel_path, server_hash) in enumerate(server_manifest.items()):
|
||||||
|
self.update_ui(text=f"Checking: {rel_path}", prog_val=i)
|
||||||
|
|
||||||
|
local_path = os.path.join(BASE_PATH, os.path.normpath(rel_path))
|
||||||
|
local_hash = self.get_md5(local_path)
|
||||||
|
|
||||||
|
if local_hash != server_hash:
|
||||||
|
files_to_download.append(rel_path)
|
||||||
|
|
||||||
|
if files_to_download:
|
||||||
|
total_dl = len(files_to_download)
|
||||||
|
self.update_ui(prog_max=total_dl, prog_val=0)
|
||||||
|
|
||||||
|
for i, rel_path in enumerate(files_to_download):
|
||||||
|
self.update_ui(text=f"Downloading ({i + 1}/{total_dl}): {rel_path}", prog_val=i)
|
||||||
|
|
||||||
|
safe_url_path = urllib.parse.quote(rel_path, safe='/')
|
||||||
|
file_url = current_server_url + safe_url_path
|
||||||
|
|
||||||
|
local_path = os.path.join(BASE_PATH, os.path.normpath(rel_path))
|
||||||
|
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||||
|
|
||||||
|
urllib.request.urlretrieve(file_url, local_path)
|
||||||
|
|
||||||
|
self.update_ui(text=f"Client '{display_name}' is up to date! Ready to play.", prog_val=self.progress['maximum'])
|
||||||
|
self.root.after(0, lambda: self.btn_play.config(state="normal"))
|
||||||
|
self.root.after(0, lambda: self.btn_update.config(state="normal"))
|
||||||
|
self.root.after(0, lambda: self.mode_combo.config(state="readonly"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.update_ui(text=f"Error: {str(e)}")
|
||||||
|
self.root.after(0, lambda: self.btn_update.config(state="normal"))
|
||||||
|
self.root.after(0, lambda: self.mode_combo.config(state="readonly"))
|
||||||
|
|
||||||
|
def play_game(self):
|
||||||
|
if os.path.exists(GAME_EXE):
|
||||||
|
game_dir = os.path.dirname(GAME_EXE)
|
||||||
|
game_exe_name = os.path.basename(GAME_EXE)
|
||||||
|
os.chdir(game_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.startfile(game_exe_name)
|
||||||
|
except AttributeError:
|
||||||
|
subprocess.Popen([os.path.join(".", game_exe_name)])
|
||||||
|
|
||||||
|
self.root.destroy()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("Error", f"Game executable not found:\n{GAME_EXE}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
root = tk.Tk()
|
||||||
|
app = UpdaterApp(root)
|
||||||
|
root.mainloop()
|
||||||
Reference in New Issue
Block a user