From 9d24e38c1d83383efb12051860e2155dfe9cbade Mon Sep 17 00:00:00 2001 From: Hikari Date: Thu, 3 Oct 2024 23:09:48 +0800 Subject: [PATCH] =?UTF-8?q?v1.5=20=E6=AD=A3=E5=BC=8F=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- README.md | 2 +- hsl-config.json | 4 +- hsl/__init__.py | 3 +- hsl/core/checks.py | 29 +++++++ hsl/core/exceptions.py | 2 + hsl/core/java.py | 22 ++--- hsl/core/main.py | 41 +++------- hsl/core/server.py | 78 +++++++++++++----- hsl/core/workspace.py | 23 +++--- hsl/gametypes/fabric.py | 46 ++++++----- hsl/gametypes/forge.py | 71 ++++++++-------- hsl/gametypes/paper.py | 22 ++--- hsl/gametypes/vanilla.py | 52 ++++++------ hsl/source/__init__.py | 1 + hsl/source/source.py | 60 ++++++++++++++ hsl/utils/download.py | 5 +- main.py | 169 +++++++++++++++++++++++---------------- source.json | 4 +- spconfigs.json | 2 +- 20 files changed, 396 insertions(+), 245 deletions(-) create mode 100644 hsl/core/checks.py create mode 100644 hsl/core/exceptions.py create mode 100644 hsl/source/__init__.py create mode 100644 hsl/source/source.py diff --git a/.gitignore b/.gitignore index 4a19686..a69f0f6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,4 @@ test*.py .ssh/ .cache/ .idea/ -pack.py -b64.txt -templates.zip -templates.py \ No newline at end of file +hsl-config.json \ No newline at end of file diff --git a/README.md b/README.md index a3f47a1..1d564c3 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Hikari Server Launcher is a simple, lightweight, and easy-to-use launcher/instal ## Contributing 贡献 - 你可以从两方面对此项目贡献: - 提交 issue, pull request 来完善这个项目。 -- 编辑configs.json,这是特定配置的索引文件,个人时间有限无法全部适配,configs.json的格式可以参考 [config.json](https://github.com/Hikari16665/HikariServerLauncher/blob/main/spconfigs.json),里面有详细的注释。 +- 编辑spconfigs.json,这是特定配置的索引文件,个人时间有限无法全部适配,spconfigs.json的格式可以参考 [spconfig.json](https://github.com/Hikari16665/HikariServerLauncher/blob/main/spconfigs.json),里面有详细的注释。 ## 致谢 Thanks diff --git a/hsl-config.json b/hsl-config.json index 7fb885f..3040683 100644 --- a/hsl-config.json +++ b/hsl-config.json @@ -3,6 +3,6 @@ "use_mirror": true, "workspace_dir": "workspace", "workspace_file": "workspace.json", - "autorun": "1", - "debug": false + "autorun": "", + "debug": true } \ No newline at end of file diff --git a/hsl/__init__.py b/hsl/__init__.py index 3082f5c..033f420 100644 --- a/hsl/__init__.py +++ b/hsl/__init__.py @@ -1,3 +1,4 @@ from . import core from . import utils -from . import gametypes \ No newline at end of file +from . import gametypes +from . import source \ No newline at end of file diff --git a/hsl/core/checks.py b/hsl/core/checks.py new file mode 100644 index 0000000..5281842 --- /dev/null +++ b/hsl/core/checks.py @@ -0,0 +1,29 @@ +import logging +import requests +from hsl.source.source import Source +logger = logging.getLogger(__name__) +DOWNLOAD_SOURCE = r'https://hsl.hikari.bond/source.json' +SPCONFIGS_SOURCE = r'https://hsl.hikari.bond/spconfigs.json' +VERSION_SOURCE = r'https://hsl.hikari.bond/hsl.json' + +def make_request(url: str, error_message: str) -> dict: + try: + response = requests.get(url) + if response.status_code == 200: + return response.json() + logger.error(f'{error_message},状态码:{response.status_code}') + return {} + except Exception as e: + logger.error(f'{error_message},错误信息:{e}') + return {} +def check_update(version: int) -> tuple[bool, int]: + data = make_request(VERSION_SOURCE, error_message='检查更新失败') + latest: int = data.get('version', 0) + return (True, latest) if version < latest else (False, version) +def load_source() -> Source: + """Load source data from sources + """ + _source = make_request(DOWNLOAD_SOURCE, error_message='加载源数据失败') + return Source(**_source) +def get_spconfigs(): + return make_request(SPCONFIGS_SOURCE, error_message='加载特定配置文件失败') \ No newline at end of file diff --git a/hsl/core/exceptions.py b/hsl/core/exceptions.py new file mode 100644 index 0000000..b94edae --- /dev/null +++ b/hsl/core/exceptions.py @@ -0,0 +1,2 @@ +class NoSuchServerException(Exception): + pass \ No newline at end of file diff --git a/hsl/core/java.py b/hsl/core/java.py index 1d5f10d..c76173d 100644 --- a/hsl/core/java.py +++ b/hsl/core/java.py @@ -54,7 +54,7 @@ async def getJavaVersion(self,mcVersion: str) -> str: return '21' else: return '0' - async def checkJavaExist(self,javaVersion,path) -> bool: + async def checkJavaExist(self,javaVersion: str,path: str) -> bool: """ Check if Java exists. Args: @@ -65,10 +65,12 @@ async def checkJavaExist(self,javaVersion,path) -> bool: bool: True if the java exists, False otherwise. """ - if not os.path.exists(os.path.join(path, 'java', javaVersion, 'bin', JAVA_EXEC)): - return False - return True - async def downloadJava(self, javaVersion, path) -> bool: + return bool( + os.path.exists( + os.path.join(path, 'java', javaVersion, 'bin', JAVA_EXEC) + ) + ) + async def downloadJava(self, javaVersion: str, path: str) -> bool: """ Download Java and return the status. @@ -79,7 +81,7 @@ async def downloadJava(self, javaVersion, path) -> bool: Returns: bool: True if the java is downloaded and extracted successfully, False otherwise. """ - sources = self.source['java']['list'] + sources = self.source.java.list if self.config.use_mirror: sources = sources[::-1] path = os.path.join(path,'java',javaVersion) @@ -90,9 +92,9 @@ async def downloadJava(self, javaVersion, path) -> bool: #get source for i in sources: if os.name == 'nt': - url = i['windows'][javaVersion] - if os.name == 'posix': - url = i['linux'][javaVersion] + url = i.windows[javaVersion] + elif os.name == 'posix': + url = i.linux[javaVersion] if await downloadfile(url,filename): with zipfile.ZipFile(filename,'r') as file: file.extractall(path) @@ -120,7 +122,7 @@ async def getJavaByGameVersion(self, mcVersion: str, path: str) -> str: await self.downloadJava(javaVersion, path) return os.path.join(javaPath, 'bin', JAVA_EXEC) - async def getJavaByJavaVersion(self, javaVersion:str, path:str) -> str: + async def getJavaByJavaVersion(self, javaVersion: str, path:str) -> str: """ get java path by java version(if not exist, will call downloadJava) diff --git a/hsl/core/main.py b/hsl/core/main.py index 5c1f178..ad23223 100644 --- a/hsl/core/main.py +++ b/hsl/core/main.py @@ -1,43 +1,20 @@ from hsl.core.config import Config +from hsl.core.checks import load_source, get_spconfigs, check_update from rich.console import Console -import logging -import requests import sys +import platform console = Console() HSL_VERSION = 15 -DOWNLOAD_SOURCE = r'https://hsl.hikari.bond/source.json' -SPCONFIGS_SOURCE = r'https://hsl.hikari.bond/spconfigs.json' -VERSION_SOURCE = r'https://hsl.hikari.bond/hsl.json' -logger = logging.getLogger(__name__) -def make_request(url: str, error_message: str) -> dict: - try: - response = requests.get(url) - if response.status_code == 200: - print(response.json()) - return response.json() - else: - logger.error(f'{error_message},状态码:{response.status_code}') - return {} - except Exception as e: - logger.error(f'{error_message},错误信息:{e}') - return {} -def check_update(version: int) -> tuple[bool, int]: - data = make_request(VERSION_SOURCE, error_message='检查更新失败') - latest: int = data.get('version', 0) - if version < latest: - return True, latest - return False, version -def load_source() -> dict: - """Load source data from sources - """ - return make_request(DOWNLOAD_SOURCE, error_message='加载源数据失败') -def get_spconfigs(): - return make_request(SPCONFIGS_SOURCE, error_message='加载特定配置文件失败') + +OS_ARCH = platform.machine() +if OS_ARCH != 'AMD64': + console.print(f'当前系统架构{OS_ARCH}不支持,请使用AMD64架构的设备/系统运行.') + sys.exit(1) console.rule('加载信息中,请稍后...') SOURCE = load_source() SPCONFIGS = get_spconfigs() -VERSIONINFO = check_update(HSL_VERSION) +VERSION_INFO = check_update(HSL_VERSION) class HSL: """Main class of HSL @@ -47,7 +24,7 @@ def __init__(self): self.config = Config().load() self.source = SOURCE self.spconfigs = SPCONFIGS - self.flag_outdated, self.latest_version = VERSIONINFO + self.flag_outdated, self.latest_version = VERSION_INFO if not self.source or not self.spconfigs: console.print('加载源数据失败,请检查网络连接.') sys.exit(1) diff --git a/hsl/core/server.py b/hsl/core/server.py index d62c615..6bfe11f 100644 --- a/hsl/core/server.py +++ b/hsl/core/server.py @@ -2,7 +2,7 @@ import psutil import asyncio import subprocess - +from hsl.core.java import Java from hsl.core.main import HSL from queue import Queue from rich.live import Live @@ -20,12 +20,14 @@ class Server(HSL): """ Server Class """ - def __init__(self, *, name: str, type: str, path: str, javaPath: str, maxRam: str, data={}): + def __init__(self, *, name: str, type: str, path: str, javaversion: str, maxRam: str, data=None): + if data is None: + data = {} super().__init__() self.name = name self.type = type self.path = path - self.javaPath = javaPath + self.javaversion = javaversion self.maxRam = maxRam self.data = data @@ -111,17 +113,22 @@ def consoleInput(self, process, input_queue: Queue): asyncio.set_event_loop(loop) loop.run_until_complete(self.get_input(process, input_queue)) - def check_process(self, process): + def check_process_exists(self, process): return psutil.pid_exists(process.pid) - def gen_run_command(self, export: bool = False) -> str: - javaexecPath = self.javaPath if os.name != 'posix' else (r'./../../' + self.javaPath if not self.config.direct_mode else r'./' + self.javaPath) - + async def gen_run_command(self, path, export: bool = False) -> str: + console.log(f'[Debug]: Path: {path}') + console.log(f'[Debug]: javaversion: {self.javaversion}') + javaexecPath = await Java().getJavaByJavaVersion(self.javaversion, path) + if export: run_dir = os.getcwd() javaexecPath = os.path.join(run_dir, javaexecPath) run_command = self._build_run_command(javaexecPath, export=True) - return f'cd {os.path.join(os.getcwd(), self.path)}\n{run_command}' + return "\n".join([ + "cd " + os.path.join(os.getcwd(), self.path), + run_command + ]) return self._build_run_command(javaexecPath) @@ -129,26 +136,58 @@ def _build_run_command(self, javaexecPath, export=False): jvm_setting = self.data.get('jvm_setting', '') if self.type in ['vanilla', 'paper', 'fabric']: - return f'{javaexecPath} -Dfile.encoding=utf-8 -Xmx{self.maxRam} -jar {self.pathJoin("server.jar")}' if export else f'{javaexecPath} -Dfile.encoding=utf-8 -Xmx{self.maxRam} -jar server.jar' - + return " ".join([ + javaexecPath, + "-Dfile.encoding=utf-8", + "-Xmx" + self.maxRam, + "-jar", + self.pathJoin("server.jar") if export else "server.jar" + ]) + mcVersion = self.data['mcVersion'] forgeVersion = self.data['forgeVersion'] mcMajorVersion = int(mcVersion.split('.')[1]) - + if mcMajorVersion >= 17: - args_path = f"@{self.pathJoin(f'libraries/net/minecraftforge/forge/{mcVersion}-{forgeVersion}/unix_args.txt')}" if os.name == 'posix' else f"@{self.pathJoin(f'libraries/net/minecraftforge/forge/{mcVersion}-{forgeVersion}/win_args.txt')}" + args_path = ( + "@" + self.pathJoin(f"libraries/net/minecraftforge/forge/{mcVersion}-{forgeVersion}/unix_args.txt") + if os.name == 'posix' + else "@" + self.pathJoin(f"libraries/net/minecraftforge/forge/{mcVersion}-{forgeVersion}/win_args.txt") + ) + if jvm_setting: console.log(f'[Debug]: Jvm Setting: {jvm_setting}') - return f'{javaexecPath}{jvm_setting} -Dfile.encoding=utf-8 -Xmx{self.maxRam} @user_jvm_args.txt {args_path} %*' - return f'{javaexecPath} -Dfile.encoding=utf-8 -Xmx{self.maxRam} @user_jvm_args.txt {args_path} ' - - return f'{javaexecPath} -Dfile.encoding=utf-8 -Xmx{self.maxRam} -jar {self.pathJoin(f"forge-{mcVersion}-{forgeVersion}.jar")}' if export else f'{javaexecPath} -Dfile.encoding=utf-8 -Xmx{self.maxRam} -jar forge-{mcVersion}-{forgeVersion}.jar' - - async def run(self): + return " ".join([ + javaexecPath + jvm_setting, + "-Dfile.encoding=utf-8", + "-Xmx" + self.maxRam, + "@user_jvm_args.txt", + args_path, + "%*" + ]) + + return " ".join([ + javaexecPath, + "-Dfile.encoding=utf-8", + "-Xmx" + self.maxRam, + "@user_jvm_args.txt", + args_path + ]) + + return " ".join([ + javaexecPath, + "-Dfile.encoding=utf-8", + "-Xmx" + self.maxRam, + "-jar", + self.pathJoin(f"forge-{mcVersion}-{forgeVersion}.jar") + if export else f"forge-{mcVersion}-{forgeVersion}.jar" + ]) + + async def run(self, path: str): if 'startup_cmd' in self.data: subprocess.Popen(self.data['startup_cmd'], cwd=self.path) - run_command = self.gen_run_command() + run_command = await self.gen_run_command(path) if self.config.debug: console.log(f'[Debug]: Run Command: {run_command}') @@ -174,4 +213,5 @@ async def run(self): t1.join() console.print('[bold green]请输入任意内容以退出控制台') t2.join() + console.print('[bold green]控制台已退出') return diff --git a/hsl/core/workspace.py b/hsl/core/workspace.py index c7a2545..67cef87 100644 --- a/hsl/core/workspace.py +++ b/hsl/core/workspace.py @@ -1,3 +1,4 @@ +from hsl.core.exceptions import NoSuchServerException from hsl.core.main import HSL import json import os @@ -24,7 +25,7 @@ def save(self): json.dump(self.workspaces, f) def load(self): with open(self.path, 'r') as f: - self.workspaces = json.load(f) + self.workspaces: list[dict] = json.load(f) async def create(self, *, server_name: str): serverPath = os.path.join(self.dir,server_name) if not os.path.exists(serverPath): @@ -37,7 +38,7 @@ async def add(self, Server: Server): "name": Server.name, "type": Server.type, "path": Server.path, - "javaPath": Server.javaPath, + "javaversion": Server.javaversion, "maxRam": Server.maxRam, "data": Server.data }) @@ -45,12 +46,12 @@ async def add(self, Server: Server): async def get(self, index: int) -> Server: server = self.workspaces[index] return Server( - name = server["name"], - type = server["type"], - path = server["path"], - javaPath = server["javaPath"], - maxRam = server["maxRam"], - data = server["data"] + name = server.get("name",''), + type = server.get("type",''), + path = server.get("path",''), + javaversion = server.get("javaversion",''), + maxRam = server.get("maxRam",''), + data = server.get("data",{}) ) async def getFromName(self, name: str) -> Server: for server in self.workspaces: @@ -59,15 +60,15 @@ async def getFromName(self, name: str) -> Server: name = server["name"], type = server["type"], path = server["path"], - javaPath = server["javaPath"], + javaversion = server["javaversion"], maxRam = server["maxRam"], data = server["data"] ) - raise Exception("Server not found") + raise NoSuchServerException("Server not found") async def delete(self, index: int): try: shutil.rmtree(self.workspaces[index]["path"]) - except: + except Exception: pass del self.workspaces[index] self.save() diff --git a/hsl/gametypes/fabric.py b/hsl/gametypes/fabric.py index 6cfe31e..52197f3 100644 --- a/hsl/gametypes/fabric.py +++ b/hsl/gametypes/fabric.py @@ -1,51 +1,53 @@ +from re import S import requests from rich.console import Console +from hsl.source.source import Source from hsl.utils.prompt import promptSelect from hsl.utils.download import downloadfile console = Console() -async def getMcVersions(source: dict) -> list: - sources = source["fabric"]['list'] - for source in sources: - if source['type'] == "official": +async def getMcVersions(source: Source) -> list[str]: + sources = source.fabric.list + for fabric_source in sources: + if fabric_source.type == "official": try: - response = requests.get(source['supportList']) + response = requests.get(fabric_source.supportList) if response.status_code == 200: fabVersions = response.json() return [i['version'] for i in fabVersions if i['stable'] == True] - except: + except Exception: pass return [] -async def getLoaderVersion(source: dict) -> str: - sources = source["fabric"]['list'] - for source in sources: - if source['type'] == "official": +async def getLoaderVersion(source: Source) -> str: + sources = source.fabric.list + for fabric_source in sources: + if fabric_source.type == "official": try: - response = requests.get(source['loaderList']) + response = requests.get(fabric_source.loaderList) if response.status_code == 200: loaderList = response.json() return loaderList[0]['version'] - except: - pass + except Exception: + pass return '' async def downloadServer( - source: dict, + source: Source, path: str, mcVersion: str, loaderVersion: str ) -> bool: - sources = source["fabric"]['list'] - for source in sources: - if source['type'] == "official": - url = source['installer'].replace(r'{version}',mcVersion).replace(r'{loader}',loaderVersion) + sources = source.fabric.list + for fabric_source in sources: + if fabric_source.type == "official": + url = fabric_source.installer.replace(r'{version}',mcVersion).replace(r'{loader}',loaderVersion) if await downloadfile(url,path): return True - return False -async def install(self, serverName: str, serverPath: str, serverJarPath: str, data: str): +async def install(self, serverName: str, serverPath: str, serverJarPath: str, data: dict): serverType = 'fabric' fabVersion = await getMcVersions(self.source) - mcVersion = fabVersion[await promptSelect(fabVersion, '请选择 Fabric 服务器版本:')] - javaPath = await self.Java.getJavaByGameVersion(mcVersion, path=self.config.workspace_dir) + fabVersion_index = await promptSelect(fabVersion, '请选择 Fabric 服务器版本:') + mcVersion = fabVersion[fabVersion_index] + javaPath = await self.Java.getJavaVersion(mcVersion) loaderVersion = await getLoaderVersion(self.source) if not await downloadServer(self.source, serverJarPath, mcVersion, loaderVersion): print('Fabric 服务端下载失败。') diff --git a/hsl/gametypes/forge.py b/hsl/gametypes/forge.py index 4203dcf..3dbc7db 100644 --- a/hsl/gametypes/forge.py +++ b/hsl/gametypes/forge.py @@ -7,63 +7,62 @@ from hsl.gametypes import vanilla from hsl.utils.download import downloadfile from hsl.utils.prompt import promptSelect +from hsl.source.source import Source FORGE_REGEX = re.compile(r'(\w+)-(\w+)') console = Console() async def nameJoin(baseurl: str,mcVersion:str, forgeversion: str,category: str,format: str): return f'{baseurl}{mcVersion}-{forgeversion}/forge-{mcVersion}-{forgeversion}-{category}.{format}' #return baseurl + forgeversion + '/forge-' + forgeversion + '-' + category + '.' + format -async def get_mcversions(source: dict,use_bmclapi: bool=False) -> list: - sources = source["forge"]['list'] +async def get_mcversions(source: Source,use_bmclapi: bool=False) -> list: + sources = source.forge.list if use_bmclapi: sources = sources[::-1] - for source in sources: - if source['type'] == 'bmclapi': + for forge_source in sources: + if forge_source.type == 'bmclapi': try: - response = requests.get(source['supportList']) + response = requests.get(forge_source.supportList) # type: ignore if response.status_code == 200: - versions = response.json() - return versions - except: + return response.json() + except Exception: pass - if source['type'] == 'official': + if forge_source.type == 'official': try: - response = requests.get(source['metadata']) + response = requests.get(forge_source.metadata) # type: ignore if response.status_code == 200: - versions = list(response.json().keys()) - return versions - except: + return list(response.json().keys()) + except Exception: pass return [] -async def get_forgeversions(source: dict,mcVersion: str,use_bmclapi: bool=False) -> list: - sources = source["forge"]['list'] +async def get_forgeversions(source: Source,mcVersion: str, use_bmclapi: bool=False) -> list: + sources = source.forge.list if use_bmclapi: sources = sources[::-1] - for source in sources: - if source['type'] == 'bmclapi': + for forge_source in sources: + if forge_source.type == 'bmclapi': try: - response = requests.get(source['getByVersion'].replace('{version}',mcVersion)) + response = requests.get(forge_source.getByVersion.replace('{version}',mcVersion)) # type: ignore if response.status_code == 200: versions = response.json() sorted_versions = sorted(versions,key=lambda i: i['build'],reverse=True) installer_versions = [i for i in sorted_versions if 'files' in i and any(j.get('format') == 'jar' and j.get('category') == 'installer' for j in i.get('files', []))] return [i['version'] for i in installer_versions] - except: + except Exception: pass - if source['type'] == 'official': + if forge_source.type == 'official': try: - response = requests.get(source['metadata']) + response = requests.get(forge_source.metadata) # type: ignore if response.status_code == 200: versions = list(response.json()[mcVersion])[::-1] return versions - except: + except Exception: pass return [] -async def download_installer(source: dict,mcVersion: str,version: str,path: str,use_bmclapi: bool=False) -> bool: - sources = source["forge"]['list'] +async def download_installer(source: Source,mcVersion: str,version: str,path: str,use_bmclapi: bool=False) -> bool: + sources = Source.forge.list if use_bmclapi: sources = sources[::-1] - for source in sources: - if source['type'] == 'bmclapi': + for forge_source in sources: + if forge_source.type == 'bmclapi': if '-' in version: mcVersion,version = re.findall(FORGE_REGEX,version) params = { @@ -72,12 +71,10 @@ async def download_installer(source: dict,mcVersion: str,version: str,path: str, 'category': 'installer', 'format': 'jar' } - status = await downloadfile(source['download'],path,params=params) - return status - if source['type'] == 'official': - url = await nameJoin(source['download'],mcVersion,version,'installer','jar') - status = await downloadfile(url, path) - return status + return await downloadfile(forge_source.download, path, params=params) # type: ignore + if forge_source.type == 'official': + url = await nameJoin(forge_source.download, mcVersion, version, 'installer', 'jar') # type: ignore + return await downloadfile(url, path) return False async def run_install(javaPath: str,path: str): cmd = f'{javaPath} -jar forge-installer.jar --installServer' @@ -91,17 +88,21 @@ async def install(self, serverName: str, serverPath: str, serverJarPath: str, da mcVersions = await vanilla.get_versions(self.source) _mcVersions = await get_mcversions(self.source, self.config.use_mirror) mcVersions = [x['id'] for x in mcVersions if x['type'] == 'release' and x['id'] in _mcVersions] + if not mcVersions: + console.print('[bold magenta]没有找到可用的 Minecraft 版本。') + return False index = await promptSelect(mcVersions, '请选择 Minecraft 版本:') mcVersion = mcVersions[index] javaPath = await self.Java.getJavaByGameVersion(mcVersion, path=self.config.workspace_dir) + javaVersion = await self.Java.getJavaVersion(mcVersion) forgeVersions = await get_forgeversions(self.source, mcVersion, self.config.use_mirror) index = await promptSelect(forgeVersions, '请选择 Forge 版本:') forgeVersion = forgeVersions[index] - #1.21-51.0.21 + #like 1.21-51.0.21 if '-' in forgeVersion: forgeVersion = forgeVersion.split('-')[1] - #51.0.21 + #split to 51.0.21 data['mcVersion'] = mcVersion data['forgeVersion'] = forgeVersion @@ -116,4 +117,4 @@ async def install(self, serverName: str, serverPath: str, serverJarPath: str, da return False console.print('Forge 安装完成。') - return serverName, serverType, serverPath, javaPath, data \ No newline at end of file + return serverName, serverType, serverPath, javaVersion, data \ No newline at end of file diff --git a/hsl/gametypes/paper.py b/hsl/gametypes/paper.py index af1db54..a2e26f8 100644 --- a/hsl/gametypes/paper.py +++ b/hsl/gametypes/paper.py @@ -1,21 +1,23 @@ from hsl.utils.download import downloadfile +from hsl.source.source import Source from rich.console import Console console = Console() -async def downloadLatest(source,path) -> bool: - sources = source["mc"]["paper"]['list'] - for source in sources: - if source['type'] == "stable": - if await downloadfile(source['latest'],path): - return True +async def downloadLatest(source: Source, path: str, experiemental=False) -> bool: + channel = 'experimental' if experiemental else 'stable' + sources = source.mc.paper.list + for paper_source in sources: + if paper_source.type == channel: + return await downloadfile(paper_source.latest,path) return False -async def getLatestVersionName(source) -> str: - return source["mc"]["paper"]['latestVersionName'] +async def getLatestVersionName(source: Source) -> str: + return source.mc.paper.latestVersionName async def install(self, serverName: str, serverPath: str, serverJarPath: str, data: dict): + experimental = data.get('experimental', False) serverType = 'paper' mcVersion = await getLatestVersionName(self.source) - javaPath = await self.Java.getJavaByGameVersion(mcVersion, path=self.config.workspace_dir) - if not await downloadLatest(self.source, serverJarPath): + javaPath = await self.Java.getJavaVersion(mcVersion) + if not await downloadLatest(self.source, serverJarPath, experiemental=experimental): console.print('Paper 服务端下载失败。') return False console.print('Paper 服务端下载完成。') diff --git a/hsl/gametypes/vanilla.py b/hsl/gametypes/vanilla.py index b7a38f9..ac60017 100644 --- a/hsl/gametypes/vanilla.py +++ b/hsl/gametypes/vanilla.py @@ -2,38 +2,44 @@ from rich.console import Console from hsl.utils.download import downloadfile from hsl.utils.prompt import promptSelect +from hsl.source.source import Source console = Console() -async def get_versions(source,mirror_first=False) -> list: - sources = source["mc"]["vanilla"]['list'] +async def get_versions(source: Source,mirror_first=False) -> list: + sources = source.mc.vanilla.list if mirror_first: sources = sources[::-1] - for source in sources: - if source['type'] == 'bmclapi': + for vanilla_source in sources: + if vanilla_source.type == 'bmclapi': try: - response = requests.get(source['versionList']) + response = requests.get(vanilla_source.versionList) if response.status_code == 200: - versions = response.json().get('versions') - return versions - except: + return response.json().get('versions') + except Exception: pass return [] -async def downloadServer(source,gameVersion,path,mirror_first=False) -> bool: - sources = source["mc"]["vanilla"]['list'] +async def downloadServer(source: Source,gameVersion,path,mirror_first=False) -> bool: + # sourcery skip: remove-redundant-pass, swap-nested-ifs + sources = source.mc.vanilla.list if mirror_first: sources = sources[::-1] - for source in sources: - if source['type'] == 'bmclapi': - if await downloadfile(source['server'].replace('{version}',gameVersion),path): + for vanilla_source in sources: + if vanilla_source.type == 'bmclapi': + if await downloadfile(vanilla_source.server.replace('{version}',gameVersion), path): return True + else: + pass return False async def install(self, serverName: str, serverPath: str, serverJarPath: str, data: dict): - serverType = 'vanilla' - mcVersions = [x['id'] for x in await get_versions(self.source) if x['type'] == 'release'] - mcVersion = mcVersions[await promptSelect(mcVersions, '请选择Minecraft服务器版本:')] - javaPath = await self.Java.getJavaByGameVersion(mcVersion, path=self.config.workspace_dir) - console.print(f'正在下载 Vanilla 服务端: {mcVersion}') - if not await downloadServer(self.source, mcVersion, serverJarPath, self.config.use_mirror): - console.print('[bold magenta]Vanilla 服务端下载失败。') - return False - console.print('Vanilla 服务端下载完成。') - return serverName, serverType, serverPath, javaPath, data \ No newline at end of file + serverType = 'vanilla' + mcVersions = [x['id'] for x in await get_versions(self.source) if x['type'] == 'release'] + if not mcVersions: + console.print('[bold magenta]没有找到可用的 Minecraft 版本。') + return False + mcVersion = mcVersions[await promptSelect(mcVersions, '请选择Minecraft服务器版本:')] + javaVersion = self.Java.getJavaVersion(mcVersion) + console.print(f'正在下载 Vanilla 服务端: {mcVersion}') + if not await downloadServer(self.source, mcVersion, serverJarPath, self.config.use_mirror): + console.print('[bold magenta]Vanilla 服务端下载失败。') + return False + console.print('Vanilla 服务端下载完成。') + return serverName, serverType, serverPath, javaVersion, data \ No newline at end of file diff --git a/hsl/source/__init__.py b/hsl/source/__init__.py new file mode 100644 index 0000000..d60e5b3 --- /dev/null +++ b/hsl/source/__init__.py @@ -0,0 +1 @@ +from . import source \ No newline at end of file diff --git a/hsl/source/source.py b/hsl/source/source.py new file mode 100644 index 0000000..7fe8bea --- /dev/null +++ b/hsl/source/source.py @@ -0,0 +1,60 @@ +from click import Option +from pydantic import BaseModel +from typing import List, Optional, Dict +class VanillaSource(BaseModel): + type: str + versionList: str + server: str +class Vanilla(BaseModel): + list: List[VanillaSource] +class PaperSource(BaseModel): + type: str + latest: str +class Paper(BaseModel): + latestVersionName: str + experimentalVersionName: str + list: List[PaperSource] +class MC(BaseModel): + vanilla: Vanilla + paper: Paper +class ForgeSource(BaseModel): + type: str + metadata: Optional[str] + download: Optional[str] + supportList: Optional[str] + getByVersion: Optional[str] + +class Forge(BaseModel): + list: List[ForgeSource] + +class NeoForgeSource(BaseModel): + type: str + getByVersion: str + download: str + +class NeoForge(BaseModel): + list: List[NeoForgeSource] + +class FabricSource(BaseModel): + type: str + supportList: str + loaderList: str + installer: str + +class Fabric(BaseModel): + list: List[FabricSource] + +class JavaSource(BaseModel): + type: str + windows: Dict[str, str] + linux: Dict[str, str] + +class Java(BaseModel): + list: List[JavaSource] + +class Source(BaseModel): + mc: MC + forge: Forge + neoforge: NeoForge + fabric: Fabric + java: Java \ No newline at end of file diff --git a/hsl/utils/download.py b/hsl/utils/download.py index d838836..19ccd18 100644 --- a/hsl/utils/download.py +++ b/hsl/utils/download.py @@ -34,6 +34,7 @@ # print(f"出现错误: {e}") # return False async def downloadfile(url: str, path: str, params=None, timeout: int = 60, threads: int = 1) -> bool: + console.log(f"Downloading {url} to {path}") try: async with httpx.AsyncClient(timeout=timeout,params=params, limits=httpx.Limits(max_connections=threads)) as Client: response = await Client.get(url=url,) @@ -47,10 +48,10 @@ async def downloadfile(url: str, path: str, params=None, timeout: int = 60, thre file.write(data) progressBar.close() if total_size != 0 and progressBar.n != total_size: - console.log(f"下载不完整 请检查网络(重新下载请删除服务器后重新安装)") + console.log("下载不完整 请检查网络(重新下载请删除服务器后重新安装)") return False else: - console.log(f"下载完成!") + console.log("下载完成!") return True else: console.log(f"无法下载 HTTP 状态码: {response.status_code} (重新下载请删除服务器后重新安装)") diff --git a/main.py b/main.py index d621b33..31972b7 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from logging import config import os import re import sys @@ -5,10 +6,11 @@ import json import asyncio import noneprompt +import winreg as reg import javaproperties -from hsl.core.java import Java from hsl.utils import osfunc from hsl.core.server import Server +from hsl.core.java import Java from typing import Callable from hsl.core.workspace import Workspace from hsl.core.main import HSL @@ -17,8 +19,8 @@ from hsl.utils.prompt import promptSelect, promptInput, promptConfirm OPTIONS_YN = ['是', '否'] -OPTIONS_ADVANCED = ['GUI测试', '取消'] -OPTIONS_SETTINGS = ['调试模式', '镜像源优先', '取消'] +OPTIONS_ADVANCED = ['取消'] +OPTIONS_SETTINGS = ['调试模式', '镜像源优先', '开机自启', '取消'] OPTIONS_GAMETYPE = ['原版','Paper','Forge','Fabric','取消'] OPTIONS_MENU = ['创建服务器', '管理服务器', '删除服务器', '设置', '高级选项', '退出'] OPTIONS_MANAGE = ['启动服务器','打开服务器目录','特定配置',"启动前执行命令",'自定义JVM设置','设定为自动启动', '导出启动脚本' ,'取消'] @@ -26,15 +28,15 @@ OS_MAXRAM = osfunc.getOSMaxRam() #max ram in MB HSL_NAME = 'Hikari Server Launcher' MAXRAM_PATTERN = re.compile(r'^\d+(\.\d+)?(M|G)$') # like 4G or 4096M - +AUTORUN_REG_HKEY = reg.HKEY_CURRENT_USER +AUTORUN_REG_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run" console = Console() -class Main(HSL): +class HSL_MAIN(HSL): def __init__(self): super().__init__() self.Workspace = Workspace() self.Java = Java() - async def welcome(self) -> None: """ Welcome @@ -47,11 +49,13 @@ async def welcome(self) -> None: console.print('设置已应用。') console.rule('配置完成') return - async def exit(self) -> None: - #exit the program sys.exit(0) - + async def do_nothing(self) -> None: + """ + Do nothing + """ + pass async def create(self) -> None: """ Create server @@ -60,20 +64,20 @@ async def create(self) -> None: if not serverName: console.print('[bold magenta]服务器已存在。') return - + console.print('服务器不存在,进入安装阶段。') serverPath = await self.Workspace.create(server_name=serverName) server_setting = await self.install(serverName=serverName, serverPath=serverPath) if not server_setting: console.print('[bold magenta]未安装服务器。') return - - serverName, serverType, serverPath, javaPath, data = server_setting # type: ignore + + serverName, serverType, serverPath, javaversion, data = server_setting # type: ignore maxRam = await self.get_valid_max_ram() - if maxRam is None: - maxRam = str(OS_MAXRAM) + 'M' - server = Server(name=serverName, type=serverType, path=serverPath, javaPath=javaPath, maxRam=maxRam, data=data) - await self.Workspace.add(server) + await self.Workspace.add( + Server(name=serverName, type=serverType, path=serverPath, javaversion=javaversion, maxRam=maxRam, data=data) + ) + console.print(f'[bold green]服务器 {serverName} 安装成功。') async def get_valid_server_name(self) -> str | None: """ @@ -83,13 +87,11 @@ async def get_valid_server_name(self) -> str | None: serverName = await promptInput('请输入服务器名称:') while (not serverName.strip()) or (serverName in ['con','aux','nul','prn'] and os.name == 'nt'): serverName = await promptInput('名称非法,请重新输入:') - + servers = self.Workspace.workspaces - if any(s['name'] == serverName for s in servers): - return None - return serverName + return None if any(s['name'] == serverName for s in servers) else serverName - async def get_valid_max_ram(self) -> str | None: + async def get_valid_max_ram(self) -> str: """ Get valid max ram Return: str | None @@ -112,22 +114,34 @@ async def install(self, *, serverName: str, serverPath: str) -> tuple | bool: serverJarPath, data """ - + if self.config.use_mirror: + console.print('[bold red]你正在使用镜像源(BMCLAPI),若无法正常下载,请切换至官方源。') serverJarPath = os.path.join(serverPath, 'server.jar') gameType = await promptSelect(OPTIONS_GAMETYPE, '请选择服务器类型:') - - install_methods: dict[int, Callable] = { - 0: vanilla.install, - 1: paper.install, - 2: forge.install, - 3: fabric.install - } - if gameType == 4: + if gameType == 0: + return await vanilla.install(self, serverName, serverPath, serverJarPath, {}) + elif gameType == 1: + data = { + 'experimental': await promptConfirm('是否下载实验性构建版本?') + } + return await paper.install(self, serverName, serverPath, serverJarPath, data) + elif gameType == 2: + return await forge.install(self, serverName, serverPath, serverJarPath, {}) + elif gameType == 3: + return await fabric.install(self, serverName, serverPath, serverJarPath, {}) + else: return False - data = {} - return await install_methods[gameType](self, serverName, serverPath, serverJarPath, data) - + # install_methods: dict[int, Callable] = { + # 0: vanilla.install, + # 1: paper.install, + # 2: forge.install, + # 3: fabric.install + # } + # if gameType == 4: + # return False + # data = {} + # return await install_methods[gameType](self, serverName, serverPath, serverJarPath, data) async def manage(self) -> None: if not self.Workspace.workspaces: console.print('[bold magenta]没有可用的服务器。') @@ -140,13 +154,14 @@ async def manage(self) -> None: index = await promptSelect(OPTIONS_MANAGE, f'{server.name} - 请选择操作:') manage_methods: dict[int, Callable] = { - 0: server.run, + 0: lambda: server.run(self.Workspace.dir), 1: lambda: self.open_server_directory(server), 2: lambda: self.editConfig(server), 3: lambda: self.set_startup_command(index), 4: lambda: self.set_jvm_settings(index), 5: lambda: self.set_autorun(server), - 6: lambda: self.export_start_script(server) + 6: lambda: self.export_start_script(server), + len(OPTIONS_MANAGE)-1: lambda: self.do_nothing() } await manage_methods[index]() return @@ -154,7 +169,7 @@ async def manage(self) -> None: async def open_server_directory(self, server: Server) -> None: try: os.startfile(server.path) - except: + except Exception: console.print('[bold magenta]无法打开服务器目录。') async def set_startup_command(self, index: int) -> None: @@ -177,9 +192,9 @@ async def set_autorun(self, server: Server) -> None: async def export_start_script(self, server: Server) -> None: script_name = f'{server.name}.run.bat' if os.name == 'nt' else f'{server.name}.run.sh' with open(script_name, 'w') as f: - f.write(server.gen_run_command(export=True)) + f.write(await server.gen_run_command(self.Workspace.dir, export=True)) console.print(f'[green]生成启动脚本成功({script_name})') - await asyncio.sleep(2) + await asyncio.sleep(3) async def editConfig(self, server: Server) -> None: console.print('[blue bold]读取特定配置索引:') @@ -258,7 +273,7 @@ async def edit_config_items(self, editConfig, config, server) -> None: key_info: dict = editConfig['keys'][editKeyIndex] console.print(f'[bold white]Tips: {key_info["tips"]}') if key_info.get('danger', False): - console.print(f'[bold red]这是一个危险配置!修改前请三思!') + console.print('[bold red]这是一个危险配置!修改前请三思!') editValue = await self.input_new_value(editConfig, key_info) self.set_nested_value(config, key.split('.'), editValue) self.save_config_file(editConfig, config, server.path) @@ -267,10 +282,7 @@ async def edit_config_items(self, editConfig, config, server) -> None: async def input_new_value(self, editConfig, key_info) -> str | int | bool | None: if key_info['type'] == "int": key = await promptInput('请输入新值(整数):') - if editConfig['type'] == 'properties': - return key - return int(key) - + return key if editConfig['type'] == 'properties' else int(key) elif key_info['type'] == "str": return await promptInput('请输入新值(字符串):') @@ -292,7 +304,7 @@ def save_config_file(self, editConfig, config, server_path) -> bool: return True else: return False - except: + except Exception: return False async def delete(self) -> None: console.rule('服务器删除') @@ -312,11 +324,34 @@ async def setting(self): settings_methods = { 0: lambda: self.set_debug_mode(), 1: lambda: self.set_mirror_priority(), - len(OPTIONS_SETTINGS) - 1: lambda: None + 2: lambda: self.set_run_on_startup(), + len(OPTIONS_SETTINGS) - 1: lambda: self.do_nothing() } - await settings_methods.get(index, lambda: None)() + await settings_methods[index]() self.config.save() + async def set_run_on_startup(self): + + if not await promptConfirm( + '是否要将 Hikari Server Launcher 设为开机自启?' + ): + return + reg_key = reg.OpenKey(AUTORUN_REG_HKEY, AUTORUN_REG_PATH, 0, reg.KEY_SET_VALUE) + query_reg_key = reg.OpenKey(AUTORUN_REG_HKEY, AUTORUN_REG_PATH, 0, reg.KEY_QUERY_VALUE) + #check if HSL is already in the registry + try: + reg.QueryValueEx(query_reg_key, HSL_NAME) + console.print('[bold green]Hikari Server Launcher 已在开机自启,无需重复设置。') + if await promptConfirm('是否移除开机自启设置?'): + reg.DeleteValue(reg_key, HSL_NAME) + return + except FileNotFoundError: + pass + if os.name == 'nt': + exec_path = os.path.abspath(sys.argv[0]) + reg.SetValueEx(reg_key, HSL_NAME, 0, reg.REG_SZ, exec_path) + else: + console.print('[bold magenta]当前系统不支持开机自启。') async def set_debug_mode(self): self.config.debug = await promptConfirm('开启调试模式?') async def set_mirror_priority(self): @@ -325,16 +360,13 @@ async def advanced_options(self): index = await promptSelect(OPTIONS_ADVANCED, '高级选项:') advanced_methods = { - 0: lambda: (), - 1: lambda: self.exit() + len(OPTIONS_ADVANCED) - 1: lambda: self.do_nothing() } await advanced_methods[index]() async def mainMenu(self): - console.clear() - console.rule(f'{HSL_NAME} v{str(self.version/10)}') + console.rule(f'{HSL_NAME} [bold blue]v{str(self.version/10)}' + (' [white]- [bold red]Debug Mode' if self.config.debug else '')) console.set_window_title(f'{HSL_NAME} v{str(self.version/10)}') - console.print(f'[bold blue]信息:当前工作目录:[u]{self.Workspace.path}[/u], 最大内存:[u]{OS_MAXRAM}[/u]MB, 调试模式:[u]{self.config.debug}[/u], 镜像源优先:[u]{self.config.use_mirror}[/u], 自动启动:[u]{self.config.autorun}[/u]') while True: console.print(f'[bold gold]欢迎使用 {HSL_NAME}.') index = await promptSelect(OPTIONS_MENU, '菜单:') @@ -353,30 +385,29 @@ async def autorun(self): server = await self.Workspace.getFromName(self.config.autorun) console.print(f'[bold blue]将于三秒后启动 {server.name}。键入Ctrl+C(^C)可取消.') await asyncio.sleep(3) - await server.run() + await server.run(self.Workspace.dir) exit() -mainProgram = Main() +mainProgram = HSL_MAIN() async def main(): isOutdated, new = mainProgram.flag_outdated, mainProgram.latest_version if isOutdated: console.print(f'[bold magenta]发现新版本,版本号:[u]{new/10}[/u],建议及时更新') - await asyncio.sleep(3) if mainProgram.config.first_run: await mainProgram.welcome() - await mainProgram.mainMenu() - else: - if mainProgram.config.autorun: - try: - loop = asyncio.get_event_loop() - task = loop.create_task(mainProgram.autorun()) - await asyncio.wait_for(task, None) - except (KeyboardInterrupt, asyncio.CancelledError): - mainProgram.config.autorun = '' - mainProgram.config.save() - console.print('自动启动已取消并重置,如需再次启用请重新设置。') - await asyncio.sleep(1) - await mainProgram.mainMenu() + if mainProgram.config.autorun: + try: + loop = asyncio.get_event_loop() + task = loop.create_task(mainProgram.autorun()) + await asyncio.wait_for(task, None) + + except (KeyboardInterrupt, asyncio.CancelledError): + mainProgram.config.autorun = '' + mainProgram.config.save() + console.print('自动启动已取消并重置,如需再次启用请重新设置。') + await asyncio.sleep(1) + + await mainProgram.mainMenu() if __name__ == '__main__': try: @@ -386,8 +417,6 @@ async def main(): except noneprompt.CancelledError: console.print('[bold green]用户取消操作,已退出。') except Exception as e: + console.print(f'[bold red]发生未知错误: {e}') if mainProgram.config.debug: - console.print_exception() - else: - console.print(f'[bold red]发生未知错误: {e}') console.print_exception() \ No newline at end of file diff --git a/source.json b/source.json index 53a7975..3c2f1fb 100644 --- a/source.json +++ b/source.json @@ -13,11 +13,11 @@ "list":[ { "type":"stable", - "latest":"https://api.papermc.io/v2/projects/paper/versions/1.21.1/builds/99/downloads/paper-1.21.1-99.jar" + "latest":"https://api.papermc.io/v2/projects/paper/versions/1.21.1/builds/118/downloads/paper-1.21.1-118.jar" }, { "type":"experimental", - "latest":"https://api.papermc.io/v2/projects/paper/versions/1.21.1/builds/99/downloads/paper-1.21.1-99.jar" + "latest":"https://api.papermc.io/v2/projects/paper/versions/1.21.1/builds/118/downloads/paper-1.21.1-118.jar" } ] } diff --git a/spconfigs.json b/spconfigs.json index e2cfc34..9887d68 100644 --- a/spconfigs.json +++ b/spconfigs.json @@ -2,7 +2,7 @@ [ { "name": "server.properties", - "?name": "! 配置文件显示在编辑处的名字", + "?name": "! 配置文件显示在特定配置编辑预览处的名字", "path": "server.properties", "?path": "! 配置文件的路径,子路径使用单个左斜线(/)分割", "description": "Minecraft服务器基本配置文件 包含端口,人数,正版等",