Some changes.

This commit is contained in:
2026-05-17 19:16:00 +08:00
parent d1663c96e9
commit 2d751f6f19
4 changed files with 290 additions and 146 deletions
+18 -4
View File
@@ -1,6 +1,7 @@
import argparse import argparse
import asyncio import asyncio
import queue import queue
import ssl
import sys import sys
import threading import threading
import socket import socket
@@ -31,6 +32,9 @@ class ServerJarClient(Application):
# Socket # Socket
self.sock = None self.sock = None
# Args
self.args = None
# event # event
self.kb = KeyBindings() self.kb = KeyBindings()
@@ -281,8 +285,9 @@ class ServerJarClient(Application):
def arguments_parser(self): def arguments_parser(self):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
# parser.add_argument("-p", "--port", type=int, help="Port number", required=True) parser.add_argument("-p", "--port", type=int, help="Port number", required=True)
# parser.add_argument('-host', '--host', type=str, help="Hostname", required=True) parser.add_argument('-host', '--host', type=str, help="Hostname", required=True)
parser.add_argument('-no-tls', '--no-tls', type="store_true", help="Enable TLS support")
args = parser.parse_args() args = parser.parse_args()
@@ -374,7 +379,16 @@ class ServerJarClient(Application):
try: try:
self._log(f"Connecting to {self.host}:{self.port} ...") self._log(f"Connecting to {self.host}:{self.port} ...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Create connect
if self.args.no_tls:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((self.host, self.port))
else:
raw = socket.create_connection((self.host, self.port))
context = ssl.create_default_context()
s = context.wrap_socket(raw, server_hostname=self.host)
s.connect((self.host, self.port)) s.connect((self.host, self.port))
with self.sock_lock: with self.sock_lock:
@@ -421,7 +435,7 @@ class ServerJarClient(Application):
def startup(self): def startup(self):
self.arguments_parser() self.args = self.arguments_parser()
self.layout.focus(self.input_area) self.layout.focus(self.input_area)
asyncio.create_task(self.consume_incoming()) asyncio.create_task(self.consume_incoming())
+236 -138
View File
@@ -10,89 +10,38 @@ import socketserver
import logging import logging
import os import os
import queue import queue
import ssl
import sys import sys
import subprocess import subprocess
import threading import threading
import time import time
import datetime
from pathlib import Path from pathlib import Path
import click import click
import yaml import yaml
from utils.common import download_latest_paper_jar, get_latest_version_minecraft, get_specific_version_paper_builds, \ from utils.common import download_latest_paper_jar, get_latest_version_minecraft, get_specific_version_paper_builds, \
download_server_jar, download_latest_build_paper_jar download_server_jar, download_latest_build_paper_jar, get_latest_paper_version
from utils.file_settings import FileSettings from utils.file_settings import FileSettings
from utils.file_settings import required_list, required_value from utils.file_settings import required_list, required_value
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
ROOT_DIR = Path(os.getcwd()) ROOT_DIR = Path(os.getcwd())
SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml" SERVER_CONFIG_PATH = ROOT_DIR / "config" / "server.yml"
VERSION = "1.0"
def exit(message): def exit(message):
click.echo(click.style(message, fg='green')) click.echo(click.style(message, fg='green'))
sys.exit(0)
@click.group() @click.group()
def main(): def main():
print("ServerJar\n" print(f"ServerJar v{VERSION}"
"WorkDir: {}".format(ROOT_DIR)) f"\nWorkDir: {ROOT_DIR}")
@main.command()
@click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name")
@click.option("--mc-version", "-m",
default=None,
help="Specify Minecraft version to download (If not specified, download latest Minecraft version)",
required=False)
@click.option("--build", "-b", default=None,
help="Specify paper build to download (Use latest Minecraft version if not specified)")
@click.option("--snapshot", is_flag=True,
help="Download snapshot version Minecraft (Use it if the current mc-version type is snapshot)")
@click.option("--latest", is_flag=True, help="Download latest Minecraft version (With latest build paper)")
@click.option("--list-builds", is_flag=True, help="List available paper build versions")
@click.option("--filename", default=None, help="Custom SERVER.jar file name")
def create_server(name, mc_version, build, snapshot, latest, list_builds, filename):
server_dir = Path("servers", name)
if server_dir.exists():
result = str(input("Found existing server dir. Do you want to overwrite it and continue? [y/N] "))
if not result.lower() == "y":
exit("User aborted.")
server_dir.mkdir(parents=True, exist_ok=True)
try:
release = True if not snapshot else False
if latest:
click.echo("Fetching latest Mojang release version...")
out = download_latest_paper_jar(server_dir, filename=filename, release=release)
click.echo(f"Done: {out}")
return
if mc_version is None:
click.echo("The mc-version is not specified. Fetching latest Minecraft release version...")
mc_version = get_latest_version_minecraft(release=release)
if list_builds:
builds = get_specific_version_paper_builds(mc_version)
if not builds:
click.echo(f"No builds found for Paper {mc_version}")
return
click.echo(f"Paper {mc_version} builds:")
click.echo(", ".join(map(str, builds[-20:])))
click.echo("(Only list latest 20 builds)")
return
if build:
click.echo(f"Downloading Paper {mc_version} build {build} ...")
out = download_server_jar(mc_version, str(build), server_dir, filename=filename)
click.echo(f"Done: {out}")
else:
click.echo(f"Downloading latest Paper build for {mc_version} ...")
out = download_latest_build_paper_jar(mc_version, server_dir, filename=filename)
click.echo(f"Done: {out}")
except Exception as e:
raise click.ClickException(str(e))
def load_settings(): def load_settings():
@@ -106,12 +55,15 @@ def load_settings():
{ {
"socketServerHostname": required_value("127.0.0.1"), "socketServerHostname": required_value("127.0.0.1"),
"socketServerPort": required_value(25560), "socketServerPort": required_value(25560),
"enableTLSSupport": required_value(True),
"socketServerCertfile": required_value("data/server-public.pem"),
"socketServerKeyfile": required_value("data/server-private.pem"),
"servers": required_list( "servers": required_list(
{ {
"name": "Unnamed Server", "name": "Unnamed Server",
"version": "unknown", "version": "unknown",
"description": "", "description": "",
"command": "", "args": [],
"workDir": "", "workDir": "",
"port": 25565, "port": 25565,
"host": "127.0.0.1", "host": "127.0.0.1",
@@ -133,14 +85,22 @@ def load_settings():
@main.command() @main.command()
@click.option("--server-folder-path", "-sf", @click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name")
help="The destination of the folder", required=True) @click.option("--mc-version", "-m",
@click.option("--server-jar-path", "-sp", default=None,
help="The destination of the SERVER.jar", required=True) help="Specify Minecraft version to download (If not specified, download latest Minecraft version)",
@click.option("--socket-server-host", "-srh", required=False)
help="Hostname of the socket server", required=True) @click.option("--build", "-b", default=None,
@click.option("--socket-server-port", "-srp", help="Specify paper build to download (Use latest Minecraft version if not specified)")
help="Port of the socket server", required=True) @click.option("--snapshot", is_flag=True,
help="Download snapshot version Minecraft (Use it if the current mc-version type is snapshot)")
@click.option("--latest", is_flag=True, help="Download latest Minecraft version (With latest build paper)")
@click.option("--list-builds", is_flag=True, help="List available paper build versions")
@click.option("--filename", default=None, help="Custom SERVER.jar file name")
@click.option("--extra-args", "-e",
help="Extra java arguments", type=str, default="")
@click.option("--custom-args", "-ce",
help="Custom arguments (command)", type=str, default="")
@click.option("--java-exec-path", "-p", show_default=True, @click.option("--java-exec-path", "-p", show_default=True,
help="The destination of the java executable", default="java") help="The destination of the java executable", default="java")
@click.option("--x-memory-initial", "-xms", show_default=True, @click.option("--x-memory-initial", "-xms", show_default=True,
@@ -152,18 +112,62 @@ def load_settings():
@click.option("--nogui", "-ng", @click.option("--nogui", "-ng",
help="Disable server window", help="Disable server window",
is_flag=True) is_flag=True)
@click.option("--extra-args", "-e", @click.option("--server-host", "-srh",
help="Extra java arguments", type=str, default="") help="Hostname of the server", required=True)
@click.option("--custom-commands", "-cd", @click.option("--server-port", "-srp",
help="Custom run commands", type=str, default="") help="Port of the server", required=True)
def create_bootstrap(server_folder_path, server_jar_path, socket_server_host, socket_server_port, def create_server(name, mc_version, build, snapshot, latest, list_builds, filename, extra_args, java_exec_path,
java_exec_path, x_memory_initial, x_memory_maximum, nogui, extra_args, custom_commands): x_memory_initial, x_memory_maximum, nogui, custom_args, server_port, server_host):
server_dir = Path("servers", name)
if server_dir.exists():
result = str(input("Found existing server dir. Do you want to overwrite it and continue? [y/N] "))
if not result.lower() == "y":
exit("User aborted.")
server_dir.mkdir(parents=True, exist_ok=True)
latest_ver = None
try:
release = True if not snapshot else False
if latest:
click.echo("Fetching latest Mojang release version...")
latest_ver = get_latest_paper_version(release=release)
builds = get_specific_version_paper_builds(latest_ver)
out = download_server_jar(latest_ver, builds[-1], server_dir)
else:
if mc_version is None:
click.echo("The mc-version is not specified. Fetching latest Minecraft release version...")
mc_version = get_latest_version_minecraft(release=release)
if list_builds:
builds = get_specific_version_paper_builds(mc_version)
if not builds:
click.echo(f"No builds found for Paper {mc_version}")
return
click.echo(f"Paper {mc_version} builds:")
click.echo(", ".join(map(str, builds[-20:])))
click.echo("(Only list latest 20 builds)")
return
if build:
click.echo(f"Downloading Paper {mc_version} build {build} ...")
out = download_server_jar(mc_version, str(build), server_dir, filename=filename)
click.echo(f"Done: {out}")
else:
click.echo(f"Downloading latest Paper build for {mc_version} ...")
out = download_latest_build_paper_jar(mc_version, server_dir, filename=filename)
click.echo(f"Done: {out}")
except Exception as e:
raise click.ClickException(str(e))
settings = load_settings() settings = load_settings()
print("There's some information you need to fill for server config.") print("There's some information you need to fill for server config.")
name = str(input("New server name: ")) name = str(input("New server name: ")) if name is None else name
version = str(input("Server version: "))
desc = str(input("Server description: ")) desc = str(input("Server description: "))
found_exist = False found_exist = False
@@ -175,33 +179,41 @@ def create_bootstrap(server_folder_path, server_jar_path, socket_server_host, so
result = str(input("WARNING: Found duplicate server name. Would you like to continue? [y/N] ")) result = str(input("WARNING: Found duplicate server name. Would you like to continue? [y/N] "))
if not result.lower() == "y": if not result.lower() == "y":
exit("User aborted.") exit("User aborted.")
return
extra_args += " nogui" if nogui else "" extra_args += "nogui" if nogui else ""
cmd = f"{java_exec_path} -Xms{x_memory_initial} -Xmx{x_memory_maximum} -jar {server_jar_path} {extra_args}" args = [
java_exec_path,
"--Xms{}".format(x_memory_initial),
"--Xmx{}".format(x_memory_maximum),
"-jar",
out.absolute().as_posix(),
extra_args,
]
if custom_commands: if custom_args:
print("Will use custom commands as replacement.") print("Will use custom commands as replacement.")
cmd = custom_commands args = custom_args
print(f"Server command: {cmd}") print(f"Server command: {" ".join(args)}")
with settings.edit() as s: with settings.edit() as s:
print("Saving...") print("Saving...")
s["servers"].append({ s["servers"].append({
"name": name, "name": name,
"version": version, "version": latest_ver if latest_ver is not None else mc_version,
"description": desc, "description": desc,
"command": cmd, "args": args,
"workDir": server_folder_path, "workDir": server_dir.absolute().as_posix(),
"port": socket_server_port, "port": server_port,
"host": socket_server_host, "host": server_host,
"enable": True, "enable": True,
}) })
print("Done") print("Done")
class SocketServer: class SocketServer:
def __init__(self, host, port): def __init__(self, host, port, enable_tls, certfile: Path, keyfile: Path):
self.logger = logging.getLogger("SocketServer") self.logger = logging.getLogger("SocketServer")
self.stdout_handler = logging.StreamHandler(sys.stdout) self.stdout_handler = logging.StreamHandler(sys.stdout)
self.stdout_handler.setFormatter(logging.Formatter("%(level)s:%(message)s")) self.stdout_handler.setFormatter(logging.Formatter("%(level)s:%(message)s"))
@@ -220,6 +232,18 @@ class SocketServer:
self._sub_lock = threading.Lock() self._sub_lock = threading.Lock()
self.command_receivers = {} self.command_receivers = {}
self.enable_tls = enable_tls
self.certfile = certfile
self.keyfile = keyfile
if not self.certfile.exists():
raise FileNotFoundError("Certfile not found")
if not self.keyfile.exists():
raise FileNotFoundError("Keyfile not found")
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
# ------------------------- # -------------------------
# Socket Server # Socket Server
@@ -253,6 +277,12 @@ class SocketServer:
def _build_tcp_server(self): def _build_tcp_server(self):
manager = self manager = self
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
try:
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
except ssl.SSLError as e:
self.logger.fatal("SSL error", exc_info=e)
class TCPServer(socketserver.ThreadingTCPServer): class TCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True allow_reuse_address = True
daemon_threads = True daemon_threads = True
@@ -261,6 +291,14 @@ class SocketServer:
super().__init__(server_address, RequestHandlerClass) super().__init__(server_address, RequestHandlerClass)
self.manager = manager self.manager = manager
if self.manager.enable_tls:
def get_request():
sock, addr = super().get_request()
tls_sock = context.wrap_socket(sock, server_side=True)
return tls_sock, addr
self.get_request = get_request
class Handler(socketserver.BaseRequestHandler): class Handler(socketserver.BaseRequestHandler):
current_server_record = { current_server_record = {
} }
@@ -442,7 +480,7 @@ class SocketServer:
return None return None
class Server: class Server:
def __init__(self, name, version, description, command, work_dir, port, host, enable): def __init__(self, name, version, description, args, work_dir, port, host, enable):
self._stdout_thread = None self._stdout_thread = None
# Process # Process
@@ -464,14 +502,14 @@ class Server:
self.logger = logging.getLogger(name) self.logger = logging.getLogger(name)
self.logger.setLevel(logging.INFO) self.logger.setLevel(logging.INFO)
self.stdout_handler = logging.StreamHandler(sys.stdout) self.stdout_handler = logging.StreamHandler(sys.stdout)
self.stdout_handler.setFormatter(logging.Formatter("[%(asctime)s:%(level)s]: %(message)s")) self.stdout_handler.setFormatter(logging.Formatter(f"[%(asctime)s:%(levelname)s:{name}]: %(message)s"))
self.logger.addHandler(self.stdout_handler) self.logger.addHandler(self.stdout_handler)
# Values from config # Values from config
self.name = name self.name = name
self.version = version self.version = version
self.description = description self.description = description
self.command = command self.args = args
self.work_dir = work_dir self.work_dir = work_dir
self.port = port self.port = port
self.host = host self.host = host
@@ -489,14 +527,12 @@ class Server:
self.logger.warning("[PROC] already running, skip") self.logger.warning("[PROC] already running, skip")
return return
args = shlex.split(self.command) if len(self.args) == 0:
if not args: raise Exception("[SYS] No arguments provided")
raise ValueError(f"Server \"{self.name}\" command is empty.")
self.logger.info("[PROC] spawning: %s", self.command)
self.logger.info("[PROC] spawning: %s", " ".join(self.args))
self.proc = subprocess.Popen( self.proc = subprocess.Popen(
args, self.args,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
@@ -634,7 +670,7 @@ def load_all_server_from_settings(settings: FileSettings):
name=server_conf.get("name"), name=server_conf.get("name"),
version=server_conf.get("version"), version=server_conf.get("version"),
description=server_conf.get("description"), description=server_conf.get("description"),
command=server_conf.get("command"), args=server_conf.get("args",[]),
work_dir=server_conf.get("workDir"), work_dir=server_conf.get("workDir"),
port=server_conf.get("port"), port=server_conf.get("port"),
host=server_conf.get("host"), host=server_conf.get("host"),
@@ -647,7 +683,7 @@ def load_all_server_from_settings(settings: FileSettings):
@main.command() @main.command()
def runserver(): def runserver():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
formatter = logging.Formatter('%(asctime)s:%(levelname)s: %(message)s') formatter = logging.Formatter('[%(asctime)s:%(levelname)s:runServer]: %(message)s')
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter) stdout_handler.setFormatter(formatter)
@@ -659,10 +695,11 @@ def runserver():
logger.info("{} servers available".format(len(servers))) logger.info("{} servers available".format(len(servers)))
# Socket # Socket
logger.info("Starting socket server")
socket_server = SocketServer(settings.get("socketServerHostname", "127.0.0.1"), socket_server = SocketServer(settings.get("socketServerHostname", "127.0.0.1"),
settings.get("socketServerPort", 25560)) settings.get("socketServerPort", 25560),
socket_server.start_socket_server() settings.get("enableTLSSupport", True),
Path(settings.get("socketServerCertfile", "data/server.crt")),
Path(settings.get( "socketServerKeyfile", "data/server.key")))
# Flags # Flags
stop_once = False stop_once = False
@@ -693,47 +730,108 @@ def runserver():
) )
# Boot server # Boot server
logger.info("Starting server") if len(servers) != 0:
for server in servers: logger.info("Starting socket server")
if server.enable: socket_server.start_socket_server()
server.register_broadcaster(socket_server.publish_log)
socket_server.register_command_receiver(server.name, server.command_receiver, logger.info("Starting server")
server.process_command_receiver) for server in servers:
try: if server.enable:
server.start() server.register_broadcaster(socket_server.publish_log)
except Exception as e: socket_server.register_command_receiver(server.name, server.command_receiver,
logger.error(f"Server {server.name} failed to start: {e}") server.process_command_receiver)
try:
server.start()
except Exception as e:
logger.error(f"Server {server.name} failed to start: {e}")
else:
logger.info(f"Server {server.name} started.")
else: else:
logger.info(f"Server {server.name} started.") logger.info(f"Server {server.name} is disabled.")
else: try:
logger.info(f"Server {server.name} is disabled.") stop = False
try:
stop = False
while not stop: while not stop:
if socket_server.stop_event.is_set(): if socket_server.stop_event.is_set():
logger.info("Remote stop event triggered. Stopping...") logger.info("Remote stop event triggered. Stopping...")
cleanup() cleanup()
stop = True stop = True
continue continue
for server in list(servers): for server in list(servers):
if server.running and not server.is_process_alive(): if server.running and not server.is_process_alive():
logger.info(f"Server {server.name} stopped.") logger.info(f"Server {server.name} stopped.")
server.running = False server.running = False
if servers and not any(server.running for server in servers): if servers and not any(server.running for server in servers):
cleanup() cleanup()
stop = True stop = True
continue continue
time.sleep(0.5) time.sleep(0.5)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Stopping server...") logger.info("Stopping server...")
cleanup() cleanup()
else:
logger.info("No work to do.")
settings.save()
logger.info("Stopped!") logger.info("Stopped!")
@main.command()
def generate_tls_key():
print("Generating TLS key...")
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"),
x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Dev Org"),
x509.NameAttribute(NameOID.COMMON_NAME, u"localhost"),
])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.now(datetime.UTC)
).not_valid_after(
# Valid for 1 year
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365)
).add_extension(
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]),
critical=False,
).sign(key, hashes.SHA256())
private_key = Path("data/server-private.pem")
public_key = Path("data/server-public.pem")
private_key.parent.mkdir(parents=True, exist_ok=True)
public_key.parent.mkdir(parents=True, exist_ok=True)
with private_key.open("wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
with public_key.open("wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print("Private key saved to {}".format(private_key))
print("Public key saved to {}".format(public_key))
print("Done.")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+29 -3
View File
@@ -9,7 +9,7 @@ MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_man
def download_file(url: str, destination: Path, chunk_size: int = 1024 * 512): def download_file(url: str, destination: Path, chunk_size: int = 1024 * 512):
destination.mkdir(parents=True, exist_ok=True) destination.parent.mkdir(parents=True, exist_ok=True)
with requests.get(url, stream=True, timeout=30) as r: with requests.get(url, stream=True, timeout=30) as r:
if r.status_code != 200: if r.status_code != 200:
@@ -17,7 +17,7 @@ def download_file(url: str, destination: Path, chunk_size: int = 1024 * 512):
total = int(r.headers.get("content-length", 0)) total = int(r.headers.get("content-length", 0))
with destination.open(mode="wb", buffering=chunk_size).write(r.content) as f: with destination.open(mode="wb", buffering=chunk_size) as f:
if total > 0: if total > 0:
with click.progressbar(length=total, label=f"Downloading {os.path.basename(destination)}") as bar: with click.progressbar(length=total, label=f"Downloading {os.path.basename(destination)}") as bar:
for chunk in r.iter_content(chunk_size=chunk_size): for chunk in r.iter_content(chunk_size=chunk_size):
@@ -72,7 +72,7 @@ def get_version_list(release=True):
def get_latest_version_minecraft(release=True): def get_latest_version_minecraft(release=True):
version_list = get_version_list(release=release) version_list = get_version_list(release=release)
ver = version_list[0].get("id") if version_list else None ver = version_list[0] if version_list else None
if ver is None: if ver is None:
raise Exception("Unable to find latest version in version list.\n") raise Exception("Unable to find latest version in version list.\n")
@@ -115,6 +115,31 @@ def download_latest_build_paper_jar(minecraft_version: str, destination_dir: Pat
build = get_latest_build_of_version(minecraft_version) build = get_latest_build_of_version(minecraft_version)
return download_server_jar(minecraft_version, build, destination_dir, filename=filename) return download_server_jar(minecraft_version, build, destination_dir, filename=filename)
def version_exist_from_paper(minecraft_version: str) -> bool:
try:
get_specific_version_paper_builds(minecraft_version)
return True
except Exception:
return False
def get_latest_paper_version(release) -> str:
vers = get_version_list(release=release)
index = 0
latest_paper_support_ver = None
while latest_paper_support_ver is None:
if len(vers) < index+1:
raise Exception("No supported Minecraft version available for Paper support.")
if version_exist_from_paper(minecraft_version=vers[index]):
latest_paper_support_ver = vers[index]
break
index+=1
return latest_paper_support_ver
def download_latest_paper_jar(destination_dir: Path, filename: str | None = None, release: bool = True): def download_latest_paper_jar(destination_dir: Path, filename: str | None = None, release: bool = True):
""" """
@@ -126,4 +151,5 @@ def download_latest_paper_jar(destination_dir: Path, filename: str | None = None
raise Exception("No versions available for Minecraft (Did the server return wrong response ?)") raise Exception("No versions available for Minecraft (Did the server return wrong response ?)")
latest_mc = vers[0] latest_mc = vers[0]
return download_latest_build_paper_jar(latest_mc, destination_dir, filename=filename) return download_latest_build_paper_jar(latest_mc, destination_dir, filename=filename)
+7 -1
View File
@@ -270,9 +270,15 @@ class FileSettings:
def _validate_dict_form(self, sample, target): def _validate_dict_form(self, sample, target):
for k, v in sample.items(): for k, v in sample.items():
if target.get(k, None) is None: if target.get(k, None) is None:
target[k] = v target[k] = copy.deepcopy(v)
elif isinstance(v, dict) and isinstance(target.get(k, None), dict): elif isinstance(v, dict) and isinstance(target.get(k, None), dict):
target[k] = self._validate_dict_form(v, target.get(k, {})) target[k] = self._validate_dict_form(v, target.get(k, {}))
elif type(target.get(k, None)) is not type(v):
candidate = target[k].get("default") if isinstance(target[k], dict) else None
if type(candidate) is type(v):
target[k] = copy.deepcopy(candidate)
else:
target[k] = copy.deepcopy(v)
return target return target