-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmc_server_setup.py
More file actions
244 lines (206 loc) · 9.6 KB
/
mc_server_setup.py
File metadata and controls
244 lines (206 loc) · 9.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import argparse
import hashlib
import json
import os
import shutil
import subprocess
import sys
import time
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
PISTON_META_MANIFEST = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
def fetch_json(url: str) -> dict:
req = Request(url, headers={"User-Agent": "Mozilla/5.0 (MCserverPy Setup)"})
with urlopen(req, timeout=60) as resp:
if resp.status != 200:
raise RuntimeError(f"HTTP {resp.status} while fetching {url}")
data = resp.read()
return json.loads(data.decode("utf-8"))
def get_version_info(version: str | None) -> tuple[str, dict]:
manifest = fetch_json(PISTON_META_MANIFEST)
if not version or version == "latest":
version_id = manifest.get("latest", {}).get("release")
if not version_id:
raise RuntimeError("Could not determine latest release from manifest")
else:
version_id = version
versions = manifest.get("versions", [])
selected = next((v for v in versions if v.get("id") == version_id), None)
if not selected:
# Try snapshot latest if asked explicitly
if version in ("latest-snapshot", "snapshot"):
snap_id = manifest.get("latest", {}).get("snapshot")
if not snap_id:
raise RuntimeError("Could not determine latest snapshot from manifest")
selected = next((v for v in versions if v.get("id") == snap_id), None)
if not selected:
raise RuntimeError(f"Snapshot {snap_id} not found in manifest")
version_id = snap_id
else:
raise RuntimeError(f"Version '{version_id}' not found in manifest")
version_meta = fetch_json(selected["url"]) # contains server download
server_download = version_meta.get("downloads", {}).get("server")
if not server_download:
raise RuntimeError(f"No server download found for version {version_id}")
return version_id, server_download
def ensure_dir(path: str):
os.makedirs(path, exist_ok=True)
def sha1_file(path: str) -> str:
h = hashlib.sha1()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def download_file(url: str, dest_path: str):
tmp_path = dest_path + ".part"
req = Request(url, headers={"User-Agent": "Mozilla/5.0 (MCserverPy Setup)"})
with urlopen(req, timeout=300) as resp, open(tmp_path, "wb") as out:
total = resp.length or 0
downloaded = 0
last_print = time.time()
while True:
chunk = resp.read(1024 * 64)
if not chunk:
break
out.write(chunk)
downloaded += len(chunk)
now = time.time()
if total and (now - last_print) > 0.5:
pct = downloaded / total * 100
print(f" Downloaded {downloaded/1_000_000:.1f}MB / {total/1_000_000:.1f}MB ({pct:.1f}%)", end="\r", flush=True)
last_print = now
# finalize
if os.path.exists(dest_path):
os.remove(dest_path)
os.replace(tmp_path, dest_path)
print("")
def write_eula(server_dir: str, accept_eula: bool):
eula_path = os.path.join(server_dir, "eula.txt")
content = (
"# By changing the setting below to TRUE you are indicating your agreement to the EULA\n"
"# https://aka.ms/MinecraftEULA\n"
f"eula={'true' if accept_eula else 'false'}\n"
)
with open(eula_path, "w", encoding="utf-8") as f:
f.write(content)
return eula_path
def write_start_script(server_dir: str, min_mem: str, max_mem: str, nogui: bool):
# Windows batch helper for convenience
cmd = f"java -Xms{min_mem} -Xmx{max_mem} -jar server.jar {'nogui' if nogui else ''}".strip()
bat_path = os.path.join(server_dir, "start.bat")
with open(bat_path, "w", encoding="utf-8") as f:
f.write("@echo off\n")
f.write("REM Generated by MCserverPy setup script\n")
f.write(cmd + "\n")
f.write("pause\n")
# Cross-platform shell script as well
sh_path = os.path.join(server_dir, "start.sh")
with open(sh_path, "w", encoding="utf-8") as f:
f.write("#!/usr/bin/env bash\n")
f.write("# Generated by MCserverPy setup script\n")
f.write(cmd + "\n")
try:
os.chmod(sh_path, 0o755)
except Exception:
pass
return bat_path, sh_path
def check_java_version() -> tuple[bool, str]:
try:
proc = subprocess.run(["java", "-version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
out = proc.stdout.strip()
# Example: 'java version "17.0.12" 2024-07-16 LTS'
version_str = ""
for line in out.splitlines():
if "version" in line and '"' in line:
version_str = line.split('"')[1]
break
is_ok = False
try:
major = int(version_str.split(".")[0])
is_ok = major >= 17
except Exception:
# fallback: allow if we can find 17 in string
is_ok = "17" in version_str or "18" in version_str or "19" in version_str or "20" in version_str or "21" in version_str
return is_ok, out
except FileNotFoundError:
return False, "java not found in PATH"
def start_server(server_dir: str, min_mem: str, max_mem: str, nogui: bool):
jar_path = os.path.join(server_dir, "server.jar")
if not os.path.isfile(jar_path):
raise RuntimeError("server.jar not found. Run setup first.")
cmd = [
"java",
f"-Xms{min_mem}",
f"-Xmx{max_mem}",
"-jar",
jar_path,
]
if nogui:
cmd.append("nogui")
print("Launching server... Press Ctrl+C to stop.")
subprocess.call(cmd, cwd=server_dir)
def main():
parser = argparse.ArgumentParser(description="Minecraft Java Edition server setup helper (vanilla)")
parser.add_argument("--version", default="latest", help="Server version to install (e.g., 1.20.4), or 'latest' (default) or 'snapshot'")
parser.add_argument("--dir", default=os.path.join(os.getcwd(), "mc_server"), help="Directory to install/setup the server")
parser.add_argument("--min-memory", default="1G", help="Initial heap size for JVM (e.g., 1G)")
parser.add_argument("--max-memory", default="2G", help="Maximum heap size for JVM (e.g., 2G)")
parser.add_argument("--nogui", action="store_true", help="Run server without GUI (recommended)")
parser.add_argument("--accept-eula", action="store_true", help="Automatically accept the Minecraft EULA (https://aka.ms/MinecraftEULA)")
parser.add_argument("--start", action="store_true", help="Start the server after setup completes")
parser.add_argument("--force", action="store_true", help="Re-download server.jar even if it exists")
args = parser.parse_args()
server_dir = os.path.abspath(args.dir)
ensure_dir(server_dir)
print(f"Target directory: {server_dir}")
ok_java, java_out = check_java_version()
if not ok_java:
print("WARNING: Java 17+ not detected. Minecraft 1.18+ requires Java 17 or newer.")
print("java -version output:")
print(java_out)
print("You can continue setup, but starting the server will likely fail until Java is installed.")
try:
print(f"Resolving Minecraft version '{args.version}'...")
version_id, server_download = get_version_info(args.version)
url = server_download.get("url")
expected_sha1 = server_download.get("sha1")
if not url:
raise RuntimeError("Server download URL missing in metadata")
print(f"Resolved version: {version_id}")
jar_path = os.path.join(server_dir, "server.jar")
if os.path.exists(jar_path) and not args.force:
print("server.jar already exists. Skipping download (use --force to re-download).")
else:
print("Downloading server.jar ...")
download_file(url, jar_path)
print("Download complete.")
if expected_sha1 and os.path.exists(jar_path):
print("Verifying SHA1...")
actual_sha1 = sha1_file(jar_path)
if actual_sha1.lower() != expected_sha1.lower():
raise RuntimeError(f"SHA1 mismatch for server.jar (got {actual_sha1}, expected {expected_sha1})")
print("SHA1 verified.")
eula_path = write_eula(server_dir, args.accept_eula)
if args.accept_eula:
print(f"EULA accepted and written to {eula_path}")
else:
print(f"EULA not accepted yet. Update {eula_path} to 'eula=true' before starting the server.")
bat_path, sh_path = write_start_script(server_dir, args.min_memory, args.max_memory, args.nogui)
print(f"Created helper scripts: {os.path.basename(bat_path)}, {os.path.basename(sh_path)}")
if args.start:
if not args.accept_eula:
print("Refusing to auto-start because EULA not accepted. Re-run with --accept-eula to start automatically.")
else:
start_server(server_dir, args.min_memory, args.max_memory, args.nogui)
print("Setup complete.")
print("Next steps:")
print(f" - Review {os.path.join(server_dir, 'eula.txt')} and ensure eula=true.")
print(f" - Start the server using start.bat (Windows) or start.sh (macOS/Linux), or run:\n java -Xms{args.min_memory} -Xmx{args.max_memory} -jar server.jar {'nogui' if args.nogui else ''}")
except (HTTPError, URLError) as e:
print(f"Network error: {e}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()