From bb5dd599d82ed4e53bc066afe7fcccbda997a7f3 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 25 Jan 2026 13:55:03 +0800 Subject: [PATCH] The Fisrt Updated --- .gitattributes | 9 + .github/workflows/build.yml | 30 + .gitignore | 58 ++ 1.py | 0 LICENSE | 121 ++++ META-INF/fabric.mod.json | 38 ++ PyFabric/__init__.py | 1 + PyFabric/command.py | 0 __init__.py | 94 +++ build.gradle | 390 +++++++++++ create_version_jars.py | 114 ++++ gradle.properties | 46 ++ gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 +++++++ gradlew.bat | 93 +++ info.json | 7 + loader.json | 48 ++ settings.gradle | 10 + .../pyfabricloader/PyFabricLoaderClient.java | 10 + .../mixin/client/ExampleClientMixin.java | 15 + .../pyfabricloader.client.mixins.json | 11 + .../gvsds/pyfabricloader/CommandHandler.java | 189 ++++++ .../gvsds/pyfabricloader/ConfigManager.java | 469 +++++++++++++ .../gvsds/pyfabricloader/PyCommandAPI.java | 430 ++++++++++++ .../gvsds/pyfabricloader/PyFabricLoader.java | 37 + .../gvsds/pyfabricloader/PythonManager.java | 634 ++++++++++++++++++ .../gvsds/pyfabricloader/VersionHelper.java | 120 ++++ .../pyfabricloader/mixin/ExampleMixin.java | 15 + .../resources/assets/pyfabricloader/icon.png | Bin 0 -> 3168 bytes src/main/resources/fabric.mod.json | 38 ++ src/main/resources/lang/en.json | 43 ++ src/main/resources/lang/zh-CN.json | 43 ++ src/main/resources/lang/zh-TW.json | 43 ++ src/main/resources/loader.json | 14 + src/main/resources/pyfabricloader.mixins.json | 14 + 35 files changed, 3439 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 1.py create mode 100644 LICENSE create mode 100644 META-INF/fabric.mod.json create mode 100644 PyFabric/__init__.py create mode 100644 PyFabric/command.py create mode 100644 __init__.py create mode 100644 build.gradle create mode 100644 create_version_jars.py create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 info.json create mode 100644 loader.json create mode 100644 settings.gradle create mode 100644 src/client/java/com/gvsds/pyfabricloader/PyFabricLoaderClient.java create mode 100644 src/client/java/com/gvsds/pyfabricloader/mixin/client/ExampleClientMixin.java create mode 100644 src/client/resources/pyfabricloader.client.mixins.json create mode 100644 src/main/java/com/gvsds/pyfabricloader/CommandHandler.java create mode 100644 src/main/java/com/gvsds/pyfabricloader/ConfigManager.java create mode 100644 src/main/java/com/gvsds/pyfabricloader/PyCommandAPI.java create mode 100644 src/main/java/com/gvsds/pyfabricloader/PyFabricLoader.java create mode 100644 src/main/java/com/gvsds/pyfabricloader/PythonManager.java create mode 100644 src/main/java/com/gvsds/pyfabricloader/VersionHelper.java create mode 100644 src/main/java/com/gvsds/pyfabricloader/mixin/ExampleMixin.java create mode 100644 src/main/resources/assets/pyfabricloader/icon.png create mode 100644 src/main/resources/fabric.mod.json create mode 100644 src/main/resources/lang/en.json create mode 100644 src/main/resources/lang/zh-CN.json create mode 100644 src/main/resources/lang/zh-TW.json create mode 100644 src/main/resources/loader.json create mode 100644 src/main/resources/pyfabricloader.mixins.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8fc3827 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +# Automatically build the project and run any configured tests for every push +# and submitted pull request. This can help catch issues that only occur on +# certain platforms or Java versions, and provides a first line of defence +# against bad commits. + +name: build +on: [pull_request, push] + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: checkout repository + uses: actions/checkout@v4 + - name: validate gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + - name: setup jdk + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'microsoft' + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + uses: actions/upload-artifact@v4 + with: + name: Artifacts + path: build/libs/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aed92c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Gradle files +.gradle +/build/ + +# IntelliJ IDEA +.idea/ +*.iml +*.ipr +*.iws + +# Eclipse +.classpath +.project +.settings/ + +# NetBeans +nbproject/ + +# macOS +.DS_Store + +# Windows +Thumbs.db + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Temporary files +*.swp +*.swo +*~ diff --git a/1.py b/1.py new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1625c17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. \ No newline at end of file diff --git a/META-INF/fabric.mod.json b/META-INF/fabric.mod.json new file mode 100644 index 0000000..76bbc6a --- /dev/null +++ b/META-INF/fabric.mod.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "id": "pyfabricloader", + "version": "1.0.0", + "name": "PyFabricLoader", + "description": "A Python mod loader for Minecraft Fabric using Jython, allowing to load and run Python mods.", + "authors": [ + "银河万通软件开发工作室 工程部 TermiNexus" + ], + "contact": { + "homepage": "https://www.gvsds.com", + "sources": "https://www.gvsds.com" + }, + "license": "MIT", + "icon": "assets/pyfabricloader/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.gvsds.pyfabricloader.PyFabricLoader" + ] + }, + "mixins": [ + "pyfabricloader.mixins.json", + { + "config": "pyfabricloader.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.14.0", + "minecraft": "[1.18.1,1.21.10]", + "java": ">=17", + "fabric-api": "*" + }, + "suggests": { + "another-mod": "*" + } +} \ No newline at end of file diff --git a/PyFabric/__init__.py b/PyFabric/__init__.py new file mode 100644 index 0000000..47a2a4f --- /dev/null +++ b/PyFabric/__init__.py @@ -0,0 +1 @@ +import command \ No newline at end of file diff --git a/PyFabric/command.py b/PyFabric/command.py new file mode 100644 index 0000000..e69de29 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..00ec035 --- /dev/null +++ b/__init__.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# PyFabricLoader Test Module + +from com.gvsds.pyfabricloader import PyCommandAPI +from java.lang import String + +ModInfos = { + "id": "test_title_module", + "name": "Test Title Module", + "version": "1.0.0", + "description": "A module to test title functionality" +} + +def ShowTitle(player, message, fadeIn=10, stay=70, fadeOut=20): + try: + command_api = PyCommandAPI.getInstance() + command_api.showTitle(player, message, fadeIn, stay, fadeOut) + return True + except Exception as e: + print("Error showing title: " + str(e)) + return False + +def ShowTitleByName(player_name, message, fadeIn=10, stay=70, fadeOut=20): + try: + command_api = PyCommandAPI.getInstance() + return command_api.showTitleByPlayerName(player_name, message, fadeIn, stay, fadeOut) + except Exception as e: + print("Error showing title by name: " + str(e)) + return False + +def ExecuteTestPy(source, message): + try: + command_api = PyCommandAPI.getInstance() + player = command_api.getPlayer(source) + if player: + success = ShowTitle(player, message) + if success: + command_api.sendFeedback(source, "Title displayed: %s" % message, False) + return 1 + else: + command_api.sendError(source, "Failed to display title") + return 0 + else: + command_api.sendError(source, "This command can only be executed by players") + return 0 + except Exception as e: + command_api.sendError(source, "Execution error: " + str(e)) + return 0 + +def ExecuteSimpleTest(source): + try: + command_api = PyCommandAPI.getInstance() + command_api.sendFeedback(source, "Simple test command executed successfully!", False) + player = command_api.getPlayer(source) + if player: + command_api.sendFeedback(source, "Current player: %s" % player.getName().getString(), False) + else: + command_api.sendError(source, "Player not found") + except Exception as e: + print("Error in simple test: " + str(e)) + +def ExecuteTitleCommand(source, arguments): + try: + command_api = PyCommandAPI.getInstance() + args = arguments.split() + if len(args) >= 3 and args[1] == "title": + player_name = args[0] + message = " ".join(args[2:]) + success = ShowTitleByName(player_name, message) + if success: + command_api.sendFeedback(source, "Title displayed to %s: %s" % (player_name, message), False) + return 1 + else: + command_api.sendError(source, "Failed to display title to %s" % player_name) + return 0 + else: + command_api.sendError(source, "Invalid command format. Use: /title-to-player title ") + return 0 + except Exception as e: + command_api.sendError(source, "Error executing command: %s" % str(e)) + return 0 + +def RegisterCommands(): + command_api = PyCommandAPI.getInstance() + command_api.registerCommandWithStringArgument("test-py", "message", True, ExecuteTestPy) + print("Successfully registered /test-py command") + command_api.registerCommandWithStringArgument("title-to-player", "Shows a title to a specified player", True, ExecuteTitleCommand) + print("Successfully registered /title-to-player command with format: /title-to-player title ") + command_api.registerSimpleCommand("simple-test", ExecuteSimpleTest) + print("Successfully registered /simple-test command") + +print("TestTitleModule initializing...") +RegisterCommands() +print("TestTitleModule initialization complete!") \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d750dee --- /dev/null +++ b/build.gradle @@ -0,0 +1,390 @@ +plugins { + id 'fabric-loom' version "${loom_version}" + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +// 定义要构建的Minecraft版本列表 +def minecraftVersions = [ + "1.18.1", "1.18.2", + "1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4", + "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6", + "1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10" +] + +// 注意:现在使用Groovy内置功能直接处理jar文件,不再需要外部工具 + +// 为每个Minecraft版本创建特定的jar文件,使用简单直接的方法 +minecraftVersions.each { mcVersion -> + def taskName = "createJarFor${mcVersion.replace('.', '_')}" + def outputDir = file("${buildDir}/libs") + def outputFile = file("${outputDir}/pyfabricloader-${project.version}-${mcVersion}.jar") + + task(taskName) { + // 不依赖Minecraft设置任务 + outputs.file(outputFile) + + doLast { + println "===== 开始生成 ${mcVersion} 版本的JAR文件 ====" + println "输出路径: ${outputFile.absolutePath}" + + // 确保输出目录存在 + outputDir.mkdirs() + println "输出目录已创建: ${outputDir.exists()}" + + // 创建临时工作目录 + def tempDir = file("${buildDir}/tmp/version_${mcVersion}") + tempDir.deleteDir() // 清理之前的临时文件 + tempDir.mkdirs() + println "临时工作目录: ${tempDir.absolutePath}" + + try { + // 创建META-INF目录 + def metaInfDir = file("${tempDir}/META-INF") + metaInfDir.mkdirs() + + // 优先使用src/main/resources目录下的fabric.mod.json + if (file("src/main/resources/fabric.mod.json").exists()) { + def originalModJson = file("src/main/resources/fabric.mod.json").text + // 替换版本占位符 + def updatedModJson = originalModJson.replace('${version}', project.version) + // 更新minecraft版本约束,使用更精确的正则表达式 + updatedModJson = updatedModJson.replaceAll(/"minecraft":\s*"\[[^\]]*\]"/, "\"minecraft\": \"${mcVersion}\"") + // 处理数组格式的情况 + updatedModJson = updatedModJson.replaceAll(/"minecraft":\s*\[.*?\]/, "\"minecraft\": \"${mcVersion}\"") + file("${metaInfDir}/fabric.mod.json").write(updatedModJson) + println "已从src/main/resources更新 fabric.mod.json" + } else if (file("META-INF/fabric.mod.json").exists()) { + def originalModJson = file("META-INF/fabric.mod.json").text + def updatedModJson = originalModJson.replaceAll(/"minecraft":\s*\["[^"]*"\]/, "\"minecraft\": \"${mcVersion}\"") + // 修复可能的入口点包路径错误 + updatedModJson = updatedModJson.replace('"com.example.pyfabric.PyFabricLoader"', '"com.gvsds.pyfabricloader.PyFabricLoader"') + file("${metaInfDir}/fabric.mod.json").write(updatedModJson) + println "已从META-INF更新 fabric.mod.json并修复入口点" + } else { + // 如果没有原始文件,创建一个基本的 + def modJsonContent = """ + { + "schemaVersion": 1, + "id": "pyfabricloader", + "version": "${project.version}", + "name": "PyFabricLoader", + "description": "Python integration for Fabric Mod Loader", + "authors": ["银河万通软件开发工作室 工程部 TermiNexus"], + "contact": { + "homepage": "https://www.gvsds.com", + "sources": "https://www.gvsds.com" + }, + "license": "MIT", + "icon": "assets/pyfabricloader/icon.png", + "environment": "*", + "entrypoints": { + "main": ["com.gvsds.pyfabricloader.PyFabricLoader"] + }, + "mixins": [ + "pyfabricloader.mixins.json", + { + "config": "pyfabricloader.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.14.0", + "minecraft": "${mcVersion}", + "java": ">=17", + "fabric-api": "*" + } + } + """ + file("${metaInfDir}/fabric.mod.json").write(modJsonContent) + println "已创建默认 fabric.mod.json 文件" + } + + // 复制Python源代码文件 + if (file("PyFabric").exists()) { + copy { + from 'PyFabric' + into file("${tempDir}/PyFabric") + } + println "已复制 PyFabric 目录中的Python文件" + } + + // 复制根目录中的Python文件 + copy { + from '.' + into tempDir + include '*.py' + include '__init__.py' + } + println "已复制根目录中的Python文件" + + // 复制src/main/resources中的资源文件 + if (file("src/main/resources").exists()) { + copy { + from 'src/main/resources' + into tempDir + } + println "已复制src/main/resources中的资源文件" + } + + // 复制编译后的Java类文件 + if (file("build/classes/java/main").exists()) { + copy { + from 'build/classes/java/main' + into tempDir + } + println "已复制编译后的Java类文件" + } + + // 确保META-INF目录存在 + metaInfDir.mkdirs() + println "确保META-INF目录存在" + + // 添加LICENSE文件 + if (file("LICENSE").exists()) { + copy { + from 'LICENSE' + into tempDir + rename { "LICENSE_pyfabricloader" } + } + println "已复制 LICENSE 文件" + } + + // 首先创建基本的JAR文件 + println "正在创建基本JAR文件..." + ant.jar(destfile: outputFile) { + fileset(dir: tempDir) + } + + // 强制包含Jython库,使用更直接的方法 + def jythonJar = file('libs/jython-slim-3.0.1-SNAPSHOT-all.jar') + if (jythonJar.exists() && jythonJar.length() > 0) { + println "Jython库存在,使用直接方法合并到JAR文件中..." + + try { + // 创建一个临时目录来存放所有文件 + def tempMergeDir = file("${buildDir}/tmp/merge_${mcVersion}") + tempMergeDir.deleteDir() + tempMergeDir.mkdirs() + + // 首先复制基本文件到合并目录 + copy { + from tempDir + into tempMergeDir + } + println "已复制基本文件到合并目录" + + // 使用Groovy的zipFile解压Jython库到合并目录 + def zipFile = new java.util.zip.ZipFile(jythonJar) + zipFile.entries().each { entry -> + if (!entry.isDirectory()) { + def targetFile = new File(tempMergeDir, entry.getName()) + targetFile.getParentFile().mkdirs() + targetFile.withOutputStream { out -> + out << zipFile.getInputStream(entry) + } + } + } + zipFile.close() + println "已成功解压Jython库到合并目录" + + // 重新创建JAR文件,包含所有内容 + ant.jar(destfile: outputFile) { + fileset(dir: tempMergeDir) { + // 排除可能的签名文件 + exclude(name: 'META-INF/*.SF') + exclude(name: 'META-INF/*.DSA') + exclude(name: 'META-INF/*.RSA') + // 排除可能冲突的库 + exclude(name: 'org/objectweb/asm/**') + exclude(name: 'com/google/common/**') + } + } + println "已成功创建包含Jython的JAR文件" + + } catch (Exception e) { + println "❌ 包含Jython库时出错: ${e.message}" + e.printStackTrace() + // 尝试使用备用方法 + try { + println "尝试使用备用方法包含Jython库..." + ant.jar(destfile: outputFile) { + fileset(dir: tempDir) + zipfileset(src: jythonJar) { + exclude(name: 'META-INF/*.SF') + exclude(name: 'META-INF/*.DSA') + exclude(name: 'META-INF/*.RSA') + exclude(name: 'org/objectweb/asm/**') + exclude(name: 'com/google/common/**') + } + } + println "备用方法成功" + } catch (Exception ex) { + println "❌ 备用方法也失败: ${ex.message}" + } + } finally { + // 清理临时目录 + if (tempMergeDir) { + tempMergeDir.deleteDir() + } + } + } else { + println "❌ Jython库文件不存在或为空" + } + + // 验证文件是否创建成功 + if (outputFile.exists()) { + println "✅ 成功创建版本特定JAR文件: ${outputFile.name}" + println "文件大小: ${outputFile.length()} 字节" + } else { + println "❌ 无法创建JAR文件,文件不存在" + } + } catch (Exception e) { + println "❌ 生成JAR文件时出错: ${e.message}" + e.printStackTrace() + } finally { + // 清理临时目录 + tempDir.deleteDir() + println "临时目录已清理" + println "===== ${mcVersion} 版本JAR文件生成完成 =====" + } + } + } +} + +// 创建一个聚合任务来为所有版本创建jar文件 +task createAllVersionJars { + dependsOn minecraftVersions.collect { "createJarFor${it.replace('.', '_')}" } + + doLast { + println "All version-specific JAR files have been generated." + } +} + +// 修改jar任务,确保它在所有版本任务之前执行 +tasks.jar.doLast { + println "Main jar created. Run createAllVersionJars to generate version-specific jars." +} + +repositories { + // 官方maven仓库 + mavenCentral() + // 其他可能需要的仓库 + maven { + name = "fabric" + url = "https://maven.fabricmc.net/" + } +} + +loom { + splitEnvironmentSourceSets() + // 确保loom和shadow插件正常工作 + + mods { + "pyfabricloader" { + sourceSet sourceSets.main + sourceSet sourceSets.client + } + } +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // Jython dependency from local libs folder + // 使用modImplementation确保编译时可用 + modImplementation files('libs/jython-slim-3.0.1-SNAPSHOT-all.jar') + +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": inputs.properties.version + } +} + +// 配置条件编译支持 +tasks.withType(JavaCompile).configureEach { + it.options.release = 17 + + it.options.compilerArgs += [ + "-Xlint:all", + "-parameters" + ] +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +jar { + inputs.property "archivesName", project.base.archivesName + + from("LICENSE") { + rename { "${it}_${inputs.properties.archivesName}"} + } +} + +// 配置jar任务以手动打包Jython依赖 +jar { + // 设置重复文件策略 + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + // 排除可能导致冲突的文件 + exclude 'META-INF/*.SF' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + + // 手动包含Jython jar的内容,但只排除冲突的库,保留所有org.python包 + from(zipTree('libs/jython-slim-3.0.1-SNAPSHOT-all.jar')) { + // 排除META-INF中的签名文件 + exclude 'META-INF/*.SF' + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + + // 排除ASM库,避免与Fabric Loader冲突 + exclude 'org/objectweb/asm/**' + exclude 'org/objectweb/asm/*' + + // 排除Google Guava库,避免与Fabric环境冲突 + exclude 'com/google/common/**' + exclude 'com/google/common/*' + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} \ No newline at end of file diff --git a/create_version_jars.py b/create_version_jars.py new file mode 100644 index 0000000..112adee --- /dev/null +++ b/create_version_jars.py @@ -0,0 +1,114 @@ +import os +import sys +import json +import zipfile +import tempfile +import shutil +import argparse + +# 支持的Minecraft版本 +MINECRAFT_VERSIONS = [ + "1.18.1", "1.18.2", "1.19.0", "1.19.1", "1.19.2", "1.19.3", "1.19.4", + "1.20.0", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6", + "1.21.0", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", + "1.21.7", "1.21.8", "1.21.9", "1.21.10" +] + +def modify_jar_for_version(input_jar, output_dir, mc_version): + """为特定Minecraft版本修改jar文件""" + # 构建输出文件名 + base_name = os.path.basename(input_jar) + name_parts = base_name.split('.') + output_name = f"{name_parts[0]}-{mc_version}.{'_'.join(name_parts[1:])}" + output_jar = os.path.join(output_dir, output_name) + + # 读取原始fabric.mod.json + original_json = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "src/main/resources/fabric.mod.json") + + if not os.path.exists(original_json): + print(f"Error: Could not find fabric.mod.json at {original_json}") + return False + + try: + # 读取并修改fabric.mod.json + with open(original_json, 'r', encoding='utf-8') as f: + mod_data = json.load(f) + + # 修改版本约束 + mod_data['depends']['minecraft'] = f'[{mc_version}]' + + # 创建临时目录 + temp_dir = tempfile.mkdtemp() + + try: + # 解压jar文件 + if not os.path.exists(input_jar): + print(f"Error: Input jar not found: {input_jar}") + return False + + with zipfile.ZipFile(input_jar, 'r') as zip_ref: + zip_ref.extractall(temp_dir) + + # 写入修改后的fabric.mod.json + meta_inf_dir = os.path.join(temp_dir, 'META-INF') + os.makedirs(meta_inf_dir, exist_ok=True) + + mod_json_path = os.path.join(meta_inf_dir, 'fabric.mod.json') + with open(mod_json_path, 'w', encoding='utf-8') as f: + json.dump(mod_data, f, indent=2) + + # 创建输出jar文件 + os.makedirs(output_dir, exist_ok=True) + with zipfile.ZipFile(output_jar, 'w', zipfile.ZIP_DEFLATED) as zip_out: + for root, dirs, files in os.walk(temp_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, temp_dir).replace('\\', '/') + zip_out.write(file_path, arcname) + + print(f"Successfully created: {output_jar}") + return True + + finally: + # 清理临时目录 + shutil.rmtree(temp_dir, ignore_errors=True) + + except Exception as e: + print(f"Error processing version {mc_version}: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description="Create version-specific JAR files for Minecraft mods") + parser.add_argument("--input-jar", required=True, help="Path to the input JAR file") + parser.add_argument("--output-dir", default="./build/libs", help="Output directory for version-specific JARs") + parser.add_argument("--version", help="Specific Minecraft version to target (optional)") + parser.add_argument("--all", action="store_true", help="Generate JARs for all supported versions") + + args = parser.parse_args() + + # 确保输出目录存在 + os.makedirs(args.output_dir, exist_ok=True) + + # 确定要处理的版本列表 + versions_to_process = [] + if args.version: + versions_to_process = [args.version] + elif args.all: + versions_to_process = MINECRAFT_VERSIONS + else: + print("Error: You must specify either --version or --all") + parser.print_help() + return 1 + + # 处理每个版本 + success_count = 0 + for version in versions_to_process: + if modify_jar_for_version(args.input_jar, args.output_dir, version): + success_count += 1 + + print(f"\nProcessing complete. Successfully created {success_count} of {len(versions_to_process)} JAR files.") + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..465a40d --- /dev/null +++ b/gradle.properties @@ -0,0 +1,46 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 +org.gradle.configuration-cache=false + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=1.19.2 +yarn_mappings=1.19.2+build.1 +yarn_mappings_1_18_1=1.18.1+build.19 +yarn_mappings_1_18_2=1.18.2+build.10 +yarn_mappings_1_19=1.19+build.38 +yarn_mappings_1_19_1=1.19.1+build.8 +yarn_mappings_1_19_2=1.19.2+build.1 +yarn_mappings_1_19_3=1.19.3+build.2 +yarn_mappings_1_19_4=1.19.4+build.8 +yarn_mappings_1_20=1.20+build.1 +yarn_mappings_1_20_1=1.20.1+build.1 +yarn_mappings_1_20_2=1.20.2+build.1 +yarn_mappings_1_20_3=1.20.3+build.1 +yarn_mappings_1_20_4=1.20.4+build.2 +yarn_mappings_1_20_5=1.20.5+build.1 +yarn_mappings_1_20_6=1.20.6+build.1 +yarn_mappings_1_21=1.21+build.1 +yarn_mappings_1_21_1=1.21.1+build.1 +yarn_mappings_1_21_2=1.21.2+build.1 +yarn_mappings_1_21_3=1.21.3+build.1 +yarn_mappings_1_21_4=1.21.4+build.1 +yarn_mappings_1_21_5=1.21.5+build.1 +yarn_mappings_1_21_6=1.21.6+build.1 +yarn_mappings_1_21_7=1.21.7+build.1 +yarn_mappings_1_21_8=1.21.8+build.1 +yarn_mappings_1_21_9=1.21.9+build.1 +yarn_mappings_1_21_10=1.21.10+build.1 +loader_version=0.14.0 +loom_version=1.11-SNAPSHOT + +# Mod Properties +mod_version=1.0.0 +maven_group=com.gvsds.pyfabricloader +archives_base_name=pyfabricloader + +# Dependencies +fabric_version=0.136.0+1.21.8 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23fd592 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=file:///D:/%E8%BF%85%E9%9B%B7%E4%B8%8B%E8%BD%BD/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/info.json b/info.json new file mode 100644 index 0000000..e5c8800 --- /dev/null +++ b/info.json @@ -0,0 +1,7 @@ +{ + "name": "TestTitleModule", + "version": "1.0.0", + "description": "Test Module - Display title information", + "pyfabric-version": "=1.0.0", + "minecraft-version": ">=1.21" +} \ No newline at end of file diff --git a/loader.json b/loader.json new file mode 100644 index 0000000..c5898e8 --- /dev/null +++ b/loader.json @@ -0,0 +1,48 @@ +{ + "Mode": { + "Mods": true, // 启用 mod 模式 + "ModSingleFileMode": true, // 启动 mod 单文件模式(即可将 .py 文件直接放在 pyfabric/mods 中) + "Libs": true, // 启用 lib 模式(扩展路径) + "Files": true // 启用 /pyfabricloader run "xxx.py" 功能 + }, + "Preload": { + "ModuleMatching": ".*\\.zip$", // 模组匹配正则表达式 *.zip + "PriorityModuleMatching": "^!.*\\.zip$", // 优先加载模组正则表达式 !*.zip + "CustomLoadOrder": [ + // ["xxx.zip", 优先级数字] + ] + }, + "Lang": "zh-CN", // zh-TW, en + "Debug": true // 启用 exec 等调试性功能 +} + +// 支持 jython class jar +// 修复 __init__.py 问题 +// 注册命令问题 +// pyfabricloader 的 lang、exec、list、都应该是管理员权限 +// 1.18+ 版本 +1.18.1 +1.18.2 +1.19 +1.19.1 +1.19.2 +1.19.3 +1.19.4 +1.20 +1.20.1 +1.20.2 +1.20.3 +1.20.4 +1.20.5 +1.20.6 +1.21 +1.21.1 +1.21.2 +1.21.3 +1.21.4 +1.21.5 +1.21.6 +1.21.7 +1.21.8 +1.21.9 +1.21.10 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..75c4d72 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} \ No newline at end of file diff --git a/src/client/java/com/gvsds/pyfabricloader/PyFabricLoaderClient.java b/src/client/java/com/gvsds/pyfabricloader/PyFabricLoaderClient.java new file mode 100644 index 0000000..a8fb72a --- /dev/null +++ b/src/client/java/com/gvsds/pyfabricloader/PyFabricLoaderClient.java @@ -0,0 +1,10 @@ +package com.gvsds.pyfabricloader; + +import net.fabricmc.api.ClientModInitializer; + +public class PyFabricLoaderClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + // This entrypoint is suitable for setting up client-specific logic, such as rendering. + } +} \ No newline at end of file diff --git a/src/client/java/com/gvsds/pyfabricloader/mixin/client/ExampleClientMixin.java b/src/client/java/com/gvsds/pyfabricloader/mixin/client/ExampleClientMixin.java new file mode 100644 index 0000000..db3383d --- /dev/null +++ b/src/client/java/com/gvsds/pyfabricloader/mixin/client/ExampleClientMixin.java @@ -0,0 +1,15 @@ +package com.gvsds.pyfabricloader.mixin.client; + +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class ExampleClientMixin { + @Inject(at = @At("HEAD"), method = "run") + private void init(CallbackInfo info) { + // This code is injected into the start of MinecraftClient.run()V + } +} \ No newline at end of file diff --git a/src/client/resources/pyfabricloader.client.mixins.json b/src/client/resources/pyfabricloader.client.mixins.json new file mode 100644 index 0000000..ac58512 --- /dev/null +++ b/src/client/resources/pyfabricloader.client.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "com.gvsds.pyfabricloader.mixin.client", + "compatibilityLevel": "JAVA_21", + "client": [ + "ExampleClientMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/src/main/java/com/gvsds/pyfabricloader/CommandHandler.java b/src/main/java/com/gvsds/pyfabricloader/CommandHandler.java new file mode 100644 index 0000000..0df4bce --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/CommandHandler.java @@ -0,0 +1,189 @@ +package com.gvsds.pyfabricloader; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; +import java.util.List; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +public class CommandHandler { + private static final ConfigManager configManager = ConfigManager.getInstance(); + private static final String MOD_VERSION = "1.0.0"; + private static final String AUTHOR = "银河万通软件开发工作室 工程部 TermiNexus"; + private static final String BILI_URL = "https://space.bilibili.com/3546619129628868"; + private static final String WEBSITE_URL = "https://www.gvsds.com"; + + public static void registerServerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { + dispatcher.register( + CommandManager.literal("pyfabricloader") + .executes(context -> showHelp(context.getSource())) + .then(CommandManager.literal("list") + .executes(context -> listMods(context.getSource()))) + .then(CommandManager.literal("reload") + .requires(source -> source.hasPermissionLevel(4)) + .executes(context -> reloadAllMods(context.getSource())) + .then(CommandManager.argument("file", StringArgumentType.greedyString()) + .executes(context -> reloadSpecificMod(context.getSource(), StringArgumentType.getString(context, "file"))))) + .then(CommandManager.literal("exec") + .then(CommandManager.argument("code", StringArgumentType.greedyString()) + .executes(context -> executePython(context.getSource(), StringArgumentType.getString(context, "code"))))) + .then(CommandManager.literal("run") + .then(CommandManager.argument("file", StringArgumentType.string()) + .executes(context -> executePythonFile(context.getSource(), StringArgumentType.getString(context, "file"))))) + .then(CommandManager.literal("help") + .executes(context -> showHelp(context.getSource()))) + .then(CommandManager.literal("about") + .executes(context -> showAbout(context.getSource()))) + .then(CommandManager.literal("lang") + .then(CommandManager.argument("language", StringArgumentType.string()) + .executes(context -> setLanguage(context.getSource(), StringArgumentType.getString(context, "language"))))) + ); + } + + private static int showHelp(ServerCommandSource source) { + StringBuilder help = new StringBuilder(); + help.append("§6").append(configManager.getTranslation("commands.help.header")).append("\n"); + help.append("§a/pyfabricloader §r- ").append(configManager.getTranslation("commands.help.main")).append("\n"); + help.append("§a/pyfabricloader list §r- ").append(configManager.getTranslation("commands.help.list")).append("\n"); + help.append("§a/pyfabricloader reload §r- ").append(configManager.getTranslation("commands.help.reload")).append("\n"); + help.append("§a/pyfabricloader reload [file.zip] §r- ").append(configManager.getTranslation("commands.help.reload_file")).append("\n"); + help.append("§a/pyfabricloader exec [代码] §r- ").append(configManager.getTranslation("commands.help.exec")).append("\n"); + help.append("§a/pyfabricloader run [文件名.py] §r- ").append(configManager.getTranslation("commands.help.run")).append("\n"); + help.append("§a/pyfabricloader help §r- ").append(configManager.getTranslation("commands.help.help")).append("\n"); + help.append("§a/pyfabricloader about §r- ").append(configManager.getTranslation("commands.help.about")).append("\n"); + help.append("§a/pyfabricloader lang [语言] §r- ").append(configManager.getTranslation("commands.help.lang")).append(" (zh-CN, zh-TW, en)"); + + sendFeedback(source, help.toString(), false); + return 1; + } + + private static int listMods(ServerCommandSource source) { + List mods = PythonManager.getInstance().getLoadedMods(); + + if (mods.isEmpty()) { + sendFeedback(source, "§6" + configManager.getTranslation("messages.no_mods"), false); + return 1; + } + + StringBuilder modList = new StringBuilder("§6" + configManager.getTranslation("messages.loaded_mods", configManager.getCurrentLanguage(), mods.size()) + "\n"); + for (PythonManager.PyModInfo mod : mods) { + modList.append("§a- §r").append(mod.getName()) + .append(" (").append(mod.getId()).append(")") + .append(" - v").append(mod.getVersion()); + if (!mod.getDescription().isEmpty()) { + modList.append("\n §7").append(mod.getDescription()); + } + modList.append("\n"); + } + + sendFeedback(source, modList.toString(), false); + return 1; + } + + private static int reloadAllMods(ServerCommandSource source) { + try { + PythonManager.getInstance().reloadAllMods(); + sendFeedback(source, "§a" + configManager.getTranslation("messages.reload_success"), false); + } catch (Exception e) { + sendFeedback(source, "§c" + configManager.getTranslation("messages.reload_failed", configManager.getCurrentLanguage(), e.getMessage()), false); + PyFabricLoader.LOGGER.error("Error reloading all mods", e); + } + return 1; + } + + private static int reloadSpecificMod(ServerCommandSource source, String fileName) { + try { + boolean success = PythonManager.getInstance().reloadMod(fileName); + if (success) { + sendFeedback(source, "§a" + configManager.getTranslation("messages.mod_reloaded", configManager.getCurrentLanguage(), fileName), false); + } else { + sendFeedback(source, "§c" + configManager.getTranslation("messages.mod_not_found", configManager.getCurrentLanguage(), fileName), false); + } + } catch (Exception e) { + sendFeedback(source, "§c" + configManager.getTranslation("messages.mod_reload_failed", configManager.getCurrentLanguage(), fileName, e.getMessage()), false); + PyFabricLoader.LOGGER.error("Error reloading mod: {}", fileName, e); + } + return 1; + } + + private static int executePython(ServerCommandSource source, String code) { + try { + String result = PythonManager.getInstance().executePython(code); + if (result.isEmpty()) { + sendFeedback(source, "§a" + configManager.getTranslation("messages.execution_success_no_output"), false); + } else { + sendFeedback(source, "§6" + configManager.getTranslation("messages.execution_result") + "\n" + result, false); + } + } catch (Exception e) { + sendFeedback(source, "§c" + configManager.getTranslation("messages.execution_error", configManager.getCurrentLanguage(), e.getMessage()), false); + PyFabricLoader.LOGGER.error("Error executing Python code", e); + } + return 1; + } + + private static int executePythonFile(ServerCommandSource source, String fileName) { + try { + String result = PythonManager.getInstance().executePythonFile(fileName); + sendFeedback(source, "§6" + configManager.getTranslation("messages.file_execution_result", configManager.getCurrentLanguage(), fileName) + "\n" + result, false); + } catch (Exception e) { + sendFeedback(source, "§c" + configManager.getTranslation("messages.file_execution_failed", configManager.getCurrentLanguage(), fileName, e.getMessage()), false); + PyFabricLoader.LOGGER.error("Error executing Python file: {}", fileName, e); + } + return 1; + } + + private static int showAbout(ServerCommandSource source) { + StringBuilder about = new StringBuilder(); + about.append("§6").append(configManager.getTranslation("messages.about.header", configManager.getCurrentLanguage(), MOD_VERSION)).append("\n"); + about.append("§a").append(configManager.getTranslation("messages.about.author", configManager.getCurrentLanguage(), AUTHOR)).append("\n"); + about.append("§b").append(configManager.getTranslation("messages.about.bilibili")).append("§r").append(BILI_URL).append("\n"); + about.append("§b").append(configManager.getTranslation("messages.about.website")).append("§r").append(WEBSITE_URL).append("\n"); + about.append("§7").append(configManager.getTranslation("messages.about.thanks")); + + sendFeedback(source, about.toString(), false); + return 1; + } + + private static int setLanguage(ServerCommandSource source, String language) { + try { + if (language.equals("zh-CN") || language.equals("zh-TW") || language.equals("en")) { + configManager.setLanguage(language); + sendFeedback(source, "§a" + configManager.getTranslation("messages.language_changed", configManager.getCurrentLanguage(), language), false); + } else { + sendFeedback(source, "§c" + configManager.getTranslation("messages.language_invalid", configManager.getCurrentLanguage(), language), false); + } + } catch (Exception e) { + sendFeedback(source, "§c" + configManager.getTranslation("messages.language_change_failed", configManager.getCurrentLanguage(), language), false); + PyFabricLoader.LOGGER.error("Error changing language to: {}", language, e); + } + return 1; + } + + private static void sendFeedback(ServerCommandSource source, String message, boolean broadcast) { + try { + Text textMessage = Text.literal(message); + + // 使用反射调用sendFeedback方法,适配不同版本 + try { + // 尝试调用1.19+版本的方法 (ServerCommandSource.sendFeedback(Supplier, boolean)) + Method sendFeedbackMethod = ServerCommandSource.class.getMethod("sendFeedback", Supplier.class, boolean.class); + sendFeedbackMethod.invoke(source, (Supplier) () -> textMessage, broadcast); + } catch (NoSuchMethodException e1) { + try { + // 尝试调用1.18.x及以下版本的方法 (ServerCommandSource.sendFeedback(Text, boolean)) + Method sendFeedbackMethod = ServerCommandSource.class.getMethod("sendFeedback", Text.class, boolean.class); + sendFeedbackMethod.invoke(source, textMessage, broadcast); + } catch (NoSuchMethodException e2) { + PyFabricLoader.LOGGER.error("No compatible sendFeedback method found", e2); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error sending feedback: {}", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gvsds/pyfabricloader/ConfigManager.java b/src/main/java/com/gvsds/pyfabricloader/ConfigManager.java new file mode 100644 index 0000000..c35bf7c --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/ConfigManager.java @@ -0,0 +1,469 @@ +package com.gvsds.pyfabricloader; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; +import java.io.*; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Configuration Manager for PyFabricLoader + * Handles loading configuration from loader.json and language files + */ +public class ConfigManager { + private static final ConfigManager INSTANCE = new ConfigManager(); + private static final String CONFIG_FILE = "loader.json"; + private static final String LANG_DIR = "lang/"; + + private JsonObject config = new JsonObject(); + private Map translations = new HashMap<>(); + private String currentLang = "zh-CN"; + private final Gson gson = new Gson(); + + private ConfigManager() { + // Private constructor for singleton + } + + public static ConfigManager getInstance() { + return INSTANCE; + } + + /** + * Initialize the configuration manager + * Loads the config and language files + */ + public void initialize() { + try { + // First try to load config from JAR resources + loadConfigFromJar(); + + // If config exists in external directory (pyfabric/configs/), load it and merge with JAR config + loadConfigFromExternal(); + + // Initialize language system + initializeLanguage(); + + PyFabricLoader.LOGGER.info("ConfigManager initialized successfully"); + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Failed to initialize ConfigManager: {}", e.getMessage(), e); + } + } + + /** + * Load configuration from JAR resources + */ + private void loadConfigFromJar() { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE)) { + if (inputStream != null) { + try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + config = gson.fromJson(reader, JsonObject.class); + PyFabricLoader.LOGGER.info("Loaded config from JAR resources"); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to load config from JAR resources: {}", e.getMessage()); + } + } + + /** + * Load configuration from external directory (pyfabric/configs/) + */ + private void loadConfigFromExternal() { + try { + // Create config directory if it doesn't exist + Path configDir = FabricLoader.getInstance().getGameDir().resolve("pyfabric/configs"); + Files.createDirectories(configDir); + + // Load external config file + Path configPath = configDir.resolve(CONFIG_FILE); + if (Files.exists(configPath)) { + try (Reader reader = Files.newBufferedReader(configPath, StandardCharsets.UTF_8)) { + JsonObject externalConfig = gson.fromJson(reader, JsonObject.class); + // Merge external config with JAR config (external takes precedence) + mergeConfigs(config, externalConfig); + PyFabricLoader.LOGGER.info("Loaded and merged external config from: {}", configPath); + } + } else { + // If external config doesn't exist, save current config to external directory + saveConfigToExternal(); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to load external config: {}", e.getMessage()); + } + } + + /** + * Save current config to external directory + */ + private void saveConfigToExternal() { + try { + Path configDir = FabricLoader.getInstance().getGameDir().resolve("pyfabric/configs"); + Files.createDirectories(configDir); + + Path configPath = configDir.resolve(CONFIG_FILE); + try (Writer writer = Files.newBufferedWriter(configPath, StandardCharsets.UTF_8)) { + gson.toJson(config, writer); + PyFabricLoader.LOGGER.info("Saved default config to: {}", configPath); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to save default config: {}", e.getMessage()); + } + } + + /** + * Merge two JSON configurations, with the second taking precedence + */ + private void mergeConfigs(JsonObject base, JsonObject override) { + for (Map.Entry entry : override.entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + + if (base.has(key) && base.get(key).isJsonObject() && value.isJsonObject()) { + // Recursively merge nested objects + mergeConfigs(base.getAsJsonObject(key), value.getAsJsonObject()); + } else { + // Override or add the value + base.add(key, value); + } + } + } + + /** + * Initialize language system + */ + private void initializeLanguage() { + // Get language from config + if (config.has("Lang")) { + currentLang = config.get("Lang").getAsString(); + } + + // Load supported languages + String[] supportedLangs = {"zh-CN", "zh-TW", "en"}; + + for (String lang : supportedLangs) { + try { + // Try to load from external directory first + loadLanguageFromExternal(lang); + + // If not found externally, load from JAR + if (!translations.containsKey(lang)) { + loadLanguageFromJar(lang); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to load language file for {}: {}", lang, e.getMessage()); + } + } + + PyFabricLoader.LOGGER.info("Language system initialized, current language: {}", currentLang); + } + + /** + * Load language file from JAR resources + */ + private void loadLanguageFromJar(String lang) { + String langFile = LANG_DIR + lang + ".json"; + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(langFile)) { + if (inputStream != null) { + try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + JsonObject langJson = gson.fromJson(reader, JsonObject.class); + translations.put(lang, langJson); + PyFabricLoader.LOGGER.info("Loaded language file from JAR: {}", lang); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to load language file from JAR: " + lang, e); + } + } + + /** + * Load language file from external directory + */ + private void loadLanguageFromExternal(String lang) { + try { + Path langDir = FabricLoader.getInstance().getGameDir().resolve("pyfabric/configs/lang"); + Files.createDirectories(langDir); + + Path langPath = langDir.resolve(lang + ".json"); + if (Files.exists(langPath)) { + try (Reader reader = Files.newBufferedReader(langPath, StandardCharsets.UTF_8)) { + JsonObject langJson = gson.fromJson(reader, JsonObject.class); + translations.put(lang, langJson); + PyFabricLoader.LOGGER.info("Loaded language file from external directory: {}", lang); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to load external language file: " + lang, e); + } + } + + /** + * Get a translated string + * @param key The translation key + * @return The translated string or the key if not found + */ + public String getTranslation(String key) { + return getTranslation(key, currentLang); + } + + /** + * Get a translated string with specific language + * @param key The translation key + * @param lang The language code + * @return The translated string or the key if not found + */ + public String getTranslation(String key, String lang) { + if (!translations.containsKey(lang)) { + // Fallback to zh-CN if requested language not available + if (!lang.equals("zh-CN")) { + return getTranslation(key, "zh-CN"); + } + return key; + } + + JsonObject langJson = translations.get(lang); + String[] parts = key.split("\\."); + JsonObject current = langJson; + + for (int i = 0; i < parts.length - 1; i++) { + if (current.has(parts[i]) && current.get(parts[i]).isJsonObject()) { + current = current.getAsJsonObject(parts[i]); + } else { + return key; + } + } + + String lastPart = parts[parts.length - 1]; + if (current.has(lastPart) && current.get(lastPart).isJsonPrimitive() && current.get(lastPart).getAsJsonPrimitive().isString()) { + return current.get(lastPart).getAsString(); + } + + return key; + } + + /** + * Get a translated string with specific language and format parameters + * This method ensures proper handling when passing a String parameter that should be used for formatting + * @param key The translation key + * @param lang The language code + * @param formatArgs The format arguments + * @return The formatted translated string + */ + public String getTranslation(String key, String lang, Object... formatArgs) { + String template = getTranslation(key, lang); + try { + return String.format(template, formatArgs); + } catch (Exception e) { + return template + " [format error]"; + } + } + + /** + * Get a formatted translated string + * @param key The translation key + * @param args The format arguments + * @return The formatted translated string + */ + public String getTranslation(String key, Object... args) { + String template = getTranslation(key); + try { + return String.format(template, args); + } catch (Exception e) { + return template + " [format error]"; + } + } + + /** + * Switch language with server command source feedback + * @param langCode The language code to switch to + * @param source The server command source for feedback + * @return True if language was switched successfully + */ + public boolean switchLanguage(String langCode, ServerCommandSource source) { + if (translations.containsKey(langCode)) { + currentLang = langCode; + + // Update language setting in config file + config.addProperty("Lang", langCode); + saveConfigToExternal(); + + if (source != null) { + sendFeedback(source, getTranslation("messages.language_changed", langCode), false); + } + + return true; + } else { + if (source != null) { + sendFeedback(source, getTranslation("messages.language_invalid", langCode), false); + } + return false; + } + } + + private void sendFeedback(ServerCommandSource source, String message, boolean broadcast) { + try { + Text textMessage = Text.literal(message); + + // Use reflection to call sendFeedback to support different Minecraft versions + try { + // Try 1.19+ version with Supplier + Method sendFeedbackMethod = ServerCommandSource.class.getMethod("sendFeedback", java.util.function.Supplier.class, boolean.class); + sendFeedbackMethod.invoke(source, (java.util.function.Supplier)() -> textMessage, broadcast); + } catch (NoSuchMethodException e1) { + // Fallback to 1.18.x version with direct Text parameter + try { + Method sendFeedbackMethod = ServerCommandSource.class.getMethod("sendFeedback", Text.class, boolean.class); + sendFeedbackMethod.invoke(source, textMessage, broadcast); + } catch (NoSuchMethodException e2) { + PyFabricLoader.LOGGER.error("Could not find sendFeedback method: {}", e2.getMessage()); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error sending feedback: {}", e.getMessage(), e); + } + } + + /** + * Set the current language + * @param lang The language code + */ + public void setLanguage(String lang) { + if (translations.containsKey(lang)) { + currentLang = lang; + config.addProperty("Lang", lang); + saveConfigToExternal(); + PyFabricLoader.LOGGER.info("Language changed to: {}", lang); + } else { + PyFabricLoader.LOGGER.warn("Requested language not available: {}", lang); + } + } + + /** + * Get current language code + * @return The current language code + */ + public String getCurrentLanguage() { + return currentLang; + } + + /** + * Get config value + * @param key The config key + * @param defaultValue The default value if key not found + * @return The config value or default + */ + public boolean getBoolean(String key, boolean defaultValue) { + JsonElement element = getConfigElement(key); + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean() + ? element.getAsBoolean() + : defaultValue; + } + + /** + * Get config value + * @param key The config key + * @param defaultValue The default value if key not found + * @return The config value or default + */ + public String getString(String key, String defaultValue) { + JsonElement element = getConfigElement(key); + return element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString() + ? element.getAsString() + : defaultValue; + } + + /** + * Get nested config element + * @param key The config key with dot notation (e.g. "Mode.Mods") + * @return The config element or null if not found + */ + private JsonElement getConfigElement(String key) { + String[] parts = key.split("\\."); + JsonObject current = config; + + for (int i = 0; i < parts.length - 1; i++) { + if (current.has(parts[i]) && current.get(parts[i]).isJsonObject()) { + current = current.getAsJsonObject(parts[i]); + } else { + return null; + } + } + + String lastPart = parts[parts.length - 1]; + return current.has(lastPart) ? current.get(lastPart) : null; + } + + /** + * Get custom load order from config + * @return List of module names in custom load order + */ + public List getCustomLoadOrder() { + List loadOrder = new ArrayList<>(); + if (config.has("Preload") && config.get("Preload").isJsonObject()) { + JsonObject preloadObj = config.getAsJsonObject("Preload"); + if (preloadObj.has("CustomLoadOrder") && preloadObj.get("CustomLoadOrder").isJsonArray()) { + JsonArray array = preloadObj.getAsJsonArray("CustomLoadOrder"); + for (int i = 0; i < array.size(); i++) { + if (array.get(i).isJsonArray()) { + JsonArray entry = array.get(i).getAsJsonArray(); + if (entry.size() > 0 && entry.get(0).isJsonPrimitive()) { + loadOrder.add(entry.get(0).getAsString()); + } + } + } + } + } + return loadOrder; + } + + /** + * Get all loaded language packs + * @return Map of language codes to language JSON objects + */ + public Map getLanguagePacks() { + return translations; + } + + /** + * Get the entire config JSON object + * @return The config JSON object + */ + public JsonObject getConfig() { + return config; + } + + /** + * Check if a specific mode is enabled in the config + * @param mode The mode name to check + * @return True if the mode is enabled + */ + public boolean isModeEnabled(String mode) { + return getBoolean("Mode." + mode, true); + } + + /** + * Get the module matching pattern from config + * @return The module matching regex pattern + */ + public String getModuleMatchingPattern() { + return getString("Preload.ModuleMatching", ".*\\.zip$"); + } + + /** + * Get the priority module matching pattern from config + * @return The priority module matching regex pattern + */ + public String getPriorityModuleMatchingPattern() { + return getString("Preload.PriorityModuleMatching", "^!.*\\.zip$"); + } + } \ No newline at end of file diff --git a/src/main/java/com/gvsds/pyfabricloader/PyCommandAPI.java b/src/main/java/com/gvsds/pyfabricloader/PyCommandAPI.java new file mode 100644 index 0000000..9e1a17c --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/PyCommandAPI.java @@ -0,0 +1,430 @@ +package com.gvsds.pyfabricloader; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +import java.util.function.Consumer; +import java.lang.reflect.Method; +import org.python.util.PythonInterpreter; + +/** + * Python Command API for PyFabricLoader + * This class provides a simplified API for Python scripts to register Minecraft commands + * without directly accessing Minecraft server command classes. + */ +public class PyCommandAPI { + private static PyCommandAPI instance; + + private PyCommandAPI() { + // Private constructor for singleton + } + + public static synchronized PyCommandAPI getInstance() { + if (instance == null) { + instance = new PyCommandAPI(); + } + return instance; + } + + /** + * Register a new command with a string argument + * @param commandName The name of the command + * @param argumentName The name of the argument + * @param isGreedy Whether the argument should consume all remaining input + * @param callback The Python callback function to execute when the command is run + */ + public void registerCommandWithStringArgument(String commandName, String argumentName, boolean isGreedy, Object callback) { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + // Create the command node + LiteralCommandNode commandNode = CommandManager + .literal(commandName) + .then(CommandManager.argument(argumentName, isGreedy ? StringArgumentType.greedyString() : StringArgumentType.string()) + .executes(context -> executePythonCallback(context, callback, argumentName))) + .build(); + + // Register the command + dispatcher.getRoot().addChild(commandNode); + PyFabricLoader.LOGGER.info("Registered command: /{}", commandName); + }); + } + + /** + * Register a simple command without arguments + * @param commandName The name of the command + * @param callback The Python callback function to execute when the command is run + */ + public void registerSimpleCommand(String commandName, Object callback) { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + // Create the command node + LiteralCommandNode commandNode = CommandManager + .literal(commandName) + .executes(context -> executePythonCallback(context, callback, null)) + .build(); + + // Register the command + dispatcher.getRoot().addChild(commandNode); + PyFabricLoader.LOGGER.info("Registered command: /{}", commandName); + }); + } + + /** + * Display a title to a player + * @param player The player to display the title to + * @param message The title message + * @param fadeIn Fade in time in ticks + * @param stay Stay time in ticks + * @param fadeOut Fade out time in ticks + */ + public void showTitle(Object player, String message, int fadeIn, int stay, int fadeOut) { + try { + if (player != null && player instanceof net.minecraft.server.network.ServerPlayerEntity) { + net.minecraft.server.network.ServerPlayerEntity serverPlayer = (net.minecraft.server.network.ServerPlayerEntity) player; + Text titleText = Text.literal(message); + + // In newer Minecraft versions, use PlayerEntity's sendMessage with title action + // Create title component with fade in, stay, fade out parameters + serverPlayer.sendMessage(titleText, true); + + // Alternative approach: Use ServerPlayNetworkHandler to send title packet + // This is a simplified version that sends a chat message instead + // For full title functionality, packet handling would be needed + PyFabricLoader.LOGGER.info("Sent title message to player: {}", message); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error showing title: {}", e.getMessage(), e); + } + } + + /** + * Display a title to a player by name + * @param playerName The name of the player to display the title to + * @param message The title message + * @param fadeIn Fade in time in ticks + * @param stay Stay time in ticks + * @param fadeOut Fade out time in ticks + * @return True if successful, false otherwise + */ + public boolean showTitleByPlayerName(String playerName, String message, int fadeIn, int stay, int fadeOut) { + try { + // Get the server instance through reflection to avoid direct dependency on specific methods + net.minecraft.server.MinecraftServer server = null; + + // Try multiple reflection approaches to get the server instance + try { + // Try getting server through FabricLoader + Class loaderClass = Class.forName("net.fabricmc.loader.api.FabricLoader"); + java.lang.reflect.Method getInstanceMethod = loaderClass.getMethod("getInstance"); + Object fabricLoader = getInstanceMethod.invoke(null); + + // Try to get server instance through Loader's game instance + try { + java.lang.reflect.Method getGameInstanceMethod = loaderClass.getMethod("getGameInstance"); + Object gameInstance = getGameInstanceMethod.invoke(fabricLoader); + if (gameInstance instanceof net.minecraft.server.MinecraftServer) { + server = (net.minecraft.server.MinecraftServer) gameInstance; + } + } catch (NoSuchMethodException e) { + PyFabricLoader.LOGGER.warn("getGameInstance method not found: {}", e.getMessage()); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Could not get server through FabricLoader: {}", e.getMessage()); + } + + // Alternative approach: Try to get server from MinecraftServer class + if (server == null) { + try { + Class serverClass = Class.forName("net.minecraft.server.MinecraftServer"); + // Try various possible methods + String[] possibleMethods = {"getServer", "getCurrentServer", "getInstance"}; + + for (String methodName : possibleMethods) { + try { + java.lang.reflect.Method method = serverClass.getMethod(methodName); + Object serverObj = method.invoke(null); + if (serverObj instanceof net.minecraft.server.MinecraftServer) { + server = (net.minecraft.server.MinecraftServer) serverObj; + break; + } + } catch (NoSuchMethodException ignored) { + // Method not found, try next one + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Could not get server instance through reflection: {}", e.getMessage()); + } + } + + // If we have a server and player name, try to find the player + if (server != null && playerName != null) { + try { + // Get the player manager + net.minecraft.server.PlayerManager playerManager = server.getPlayerManager(); + if (playerManager != null) { + // Get player by name + net.minecraft.server.network.ServerPlayerEntity player = playerManager.getPlayer(playerName); + if (player != null) { + // Use existing showTitle method to display the title + showTitle(player, message, fadeIn, stay, fadeOut); + return true; + } else { + PyFabricLoader.LOGGER.warn("Player not found: {}", playerName); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error accessing player manager: {}", e.getMessage(), e); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error in showTitleByPlayerName: {}", e.getMessage(), e); + } + return false; + } + + /** + * Send a feedback message to the command source + * @param source The command source + * @param message The message to send + * @param broadcast Whether to broadcast to all operators + */ + public void sendFeedback(Object source, String message, boolean broadcast) { + try { + if (source != null && source instanceof ServerCommandSource) { + ServerCommandSource commandSource = (ServerCommandSource) source; + Text textMessage = Text.literal(message); + + try { + // Try to use the method directly with Text parameter first (1.18.x) + commandSource.getClass().getMethod("sendFeedback", Text.class, boolean.class) + .invoke(commandSource, textMessage, broadcast); + } catch (NoSuchMethodException e) { + // If that fails, try with Supplier parameter (1.19+) + try { + // 先创建Supplier对象再传递给反射调用 + java.util.function.Supplier textSupplier = () -> textMessage; + commandSource.getClass().getMethod("sendFeedback", java.util.function.Supplier.class, boolean.class) + .invoke(commandSource, textSupplier, broadcast); + } catch (Exception ex) { + PyFabricLoader.LOGGER.error("Failed to send feedback: {}", ex.getMessage()); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Failed to send feedback: {}", e.getMessage()); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error sending feedback: {}", e.getMessage(), e); + } + } + + /** + * Send an error message to the command source + * @param source The command source + * @param message The error message to send + */ + public void sendError(Object source, String message) { + try { + if (source != null && source instanceof ServerCommandSource) { + ServerCommandSource commandSource = (ServerCommandSource) source; + Text textMessage = Text.literal(message); + + //#if MC>=11900 + // 1.19+版本使用函数式接口参数 + commandSource.sendError(textMessage); + //#else + // 1.18.x版本直接使用Text参数 + commandSource.sendError(textMessage); + //#endif + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error sending error message: {}", e.getMessage(), e); + } + } + + /** + * Get the player from a command source + * @param source The command source + * @return The player object or null if not a player + */ + public Object getPlayer(Object source) { + try { + if (source != null && source instanceof ServerCommandSource) { + ServerCommandSource commandSource = (ServerCommandSource) source; + return commandSource.getPlayer(); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error getting player: {}", e.getMessage(), e); + } + return null; + } + + /** + * Get a player by name + * @param playerName The name of the player to find + * @return The player object or null if not found + */ + public Object getPlayerByName(String playerName) { + try { + // Get the server instance through reflection to avoid direct dependency + net.minecraft.server.MinecraftServer server = null; + + // Try multiple reflection approaches to get the server instance + try { + // Try getting server through FabricLoader + Class loaderClass = Class.forName("net.fabricmc.loader.api.FabricLoader"); + java.lang.reflect.Method getInstanceMethod = loaderClass.getMethod("getInstance"); + Object fabricLoader = getInstanceMethod.invoke(null); + + // Try to get server instance through Loader's game instance + try { + java.lang.reflect.Method getGameInstanceMethod = loaderClass.getMethod("getGameInstance"); + Object gameInstance = getGameInstanceMethod.invoke(fabricLoader); + if (gameInstance instanceof net.minecraft.server.MinecraftServer) { + server = (net.minecraft.server.MinecraftServer) gameInstance; + } + } catch (NoSuchMethodException e) { + // Method not found, continue + } + } catch (Exception e) { + // Continue with other methods + } + + // Alternative approach: Try to get server from MinecraftServer class + if (server == null) { + try { + Class serverClass = Class.forName("net.minecraft.server.MinecraftServer"); + // Try various possible methods + String[] possibleMethods = {"getServer", "getCurrentServer", "getInstance"}; + + for (String methodName : possibleMethods) { + try { + java.lang.reflect.Method method = serverClass.getMethod(methodName); + Object serverObj = method.invoke(null); + if (serverObj instanceof net.minecraft.server.MinecraftServer) { + server = (net.minecraft.server.MinecraftServer) serverObj; + break; + } + } catch (NoSuchMethodException ignored) { + // Method not found, try next one + } + } + } catch (Exception e) { + // Continue + } + } + + if (server != null && playerName != null) { + try { + // Get the player manager + net.minecraft.server.PlayerManager playerManager = server.getPlayerManager(); + if (playerManager != null) { + // Get player by name + return playerManager.getPlayer(playerName); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error accessing player manager: {}", e.getMessage(), e); + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error getting player by name: {}", e.getMessage(), e); + } + return null; + } + + /** + * Execute a Python callback function when a command is run + */ + private int executePythonCallback(CommandContext context, Object callback, String argumentName) { + try { + // Get the command source + ServerCommandSource source = context.getSource(); + + // Get the argument value if specified + Object argumentValue = null; + if (argumentName != null) { + argumentValue = StringArgumentType.getString(context, argumentName); + } + + // Call the Python callback function using Jython's PyObject interface + // This is the most reliable way to call Python functions from Java + Object result = null; + + // Import Jython PyObject class dynamically to avoid direct dependency issues + try { + // Try direct approach first - for Jython 2.x compatibility + if (argumentName != null) { + // With argument - use PythonInterpreter's call helper method + // Create a new PythonInterpreter instance for this call + org.python.util.PythonInterpreter interpreter = new org.python.util.PythonInterpreter(); + try { + // Use exec to call the function with arguments + interpreter.set("__callback", callback); + interpreter.set("__source", source); + interpreter.set("__arg", argumentValue); + interpreter.exec("__result = __callback(__source, __arg)"); + result = interpreter.get("__result"); + } finally { + interpreter.cleanup(); + } + } else { + // Without argument + org.python.util.PythonInterpreter interpreter = new org.python.util.PythonInterpreter(); + try { + interpreter.set("__callback", callback); + interpreter.set("__source", source); + interpreter.exec("__result = __callback(__source)"); + result = interpreter.get("__result"); + } finally { + interpreter.cleanup(); + } + } + } catch (Exception e) { + // Fallback to reflection with different signatures + Method[] methods = callback.getClass().getMethods(); + for (Method method : methods) { + if (method.getName().equals("__call__")) { + try { + // Try with different parameter combinations + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length == 2 && argumentName != null) { + result = method.invoke(callback, source, argumentValue); + break; + } else if (paramTypes.length == 1) { + result = method.invoke(callback, source); + break; + } else if (paramTypes.length == 1 && paramTypes[0].isArray()) { + // Try with array parameter + if (argumentName != null) { + result = method.invoke(callback, new Object[]{new Object[]{source, argumentValue}}); + } else { + result = method.invoke(callback, new Object[]{new Object[]{source}}); + } + break; + } + } catch (IllegalArgumentException ex) { + // This signature doesn't match, continue + continue; + } + } + } + } + + // Convert result to integer (command result code) + if (result instanceof Number) { + return ((Number) result).intValue(); + } + return 1; // Default success + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error executing Python command callback: {}", e.getMessage(), e); + return 0; // Error + } + } +} diff --git a/src/main/java/com/gvsds/pyfabricloader/PyFabricLoader.java b/src/main/java/com/gvsds/pyfabricloader/PyFabricLoader.java new file mode 100644 index 0000000..7af80ae --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/PyFabricLoader.java @@ -0,0 +1,37 @@ +package com.gvsds.pyfabricloader; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PyFabricLoader implements ModInitializer { + public static final String MOD_ID = "pyfabricloader"; + + // This logger is used to write text to the console and the log file. + // It is considered best practice to use your mod id as the logger's name. + // That way, it's clear which mod wrote info, warnings, and errors. + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + @Override + public void onInitialize() { + // This code runs as soon as Minecraft is in a mod-load-ready state. + // However, some things (like resources) may still be uninitialized. + // Proceed with mild caution. + + LOGGER.info("Initializing PyFabricLoader..."); + + // 初始化ConfigManager + ConfigManager.getInstance().initialize(); + + // 初始化Python管理器 + PythonManager.getInstance().initialize(); + + // 注册命令 + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + CommandHandler.registerServerCommands(dispatcher, registryAccess); + }); + + LOGGER.info("PyFabricLoader initialized successfully!"); + } +} \ No newline at end of file diff --git a/src/main/java/com/gvsds/pyfabricloader/PythonManager.java b/src/main/java/com/gvsds/pyfabricloader/PythonManager.java new file mode 100644 index 0000000..ff0613c --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/PythonManager.java @@ -0,0 +1,634 @@ +package com.gvsds.pyfabricloader; + +import org.python.core.*; +import org.python.util.PythonInterpreter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.*; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import com.gvsds.pyfabricloader.ConfigManager; +import com.google.gson.*; +import java.io.InputStreamReader; +import java.io.InputStream; +import net.fabricmc.loader.api.Version; + +public class PythonManager { + private static final PythonManager INSTANCE = new PythonManager(); + private final Map loadedMods = new HashMap<>(); + private final Map interpreters = new HashMap<>(); + private final File modsDir; + private final File configsDir; + private final File libsDir; + private final File filesDir; + private PythonInterpreter globalInterpreter; + + private PythonManager() { + // 初始化工作目录 + File pyfabricDir = new File("pyfabric"); + modsDir = new File(pyfabricDir, "mods"); + configsDir = new File(pyfabricDir, "configs"); + libsDir = new File(pyfabricDir, "libs"); + filesDir = new File(pyfabricDir, "files"); + } + + /** + * 检查版本是否满足条件 + * 支持的格式: + * - >=1.20.1 + * - <1.20.1 + * - =1.20.1 + * - !=["1.20.1", "1.34.7"] + */ + private boolean checkVersion(String condition, String currentVersion) { + try { + if (condition == null || condition.isEmpty()) { + return true; + } + + // 处理不等于多个版本的情况 + if (condition.startsWith("!=[")) { + JsonArray versionsArray = JsonParser.parseString(condition).getAsJsonArray(); + for (JsonElement element : versionsArray) { + if (currentVersion.equals(element.getAsString())) { + return false; + } + } + return true; + } + + // 处理其他比较操作 + if (condition.startsWith(">=")) { + return compareVersions(currentVersion, condition.substring(2)) >= 0; + } else if (condition.startsWith("<=")) { + return compareVersions(currentVersion, condition.substring(2)) <= 0; + } else if (condition.startsWith(">")) { + return compareVersions(currentVersion, condition.substring(1)) > 0; + } else if (condition.startsWith("<")) { + return compareVersions(currentVersion, condition.substring(1)) < 0; + } else if (condition.startsWith("=")) { + return currentVersion.equals(condition.substring(1)); + } + + // 如果没有前缀,则默认是等于 + return currentVersion.equals(condition); + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to check version condition {} for {}", condition, currentVersion, e); + return false; + } + } + + /** + * 比较两个版本字符串 + * @return 0 if equal, 1 if version1 > version2, -1 if version1 < version2 + */ + private int compareVersions(String version1, String version2) { + try { + String[] parts1 = version1.split("\\."); + String[] parts2 = version2.split("\\."); + + int maxLength = Math.max(parts1.length, parts2.length); + + for (int i = 0; i < maxLength; i++) { + int num1 = i < parts1.length ? Integer.parseInt(parts1[i].replaceAll("\\D", "")) : 0; + int num2 = i < parts2.length ? Integer.parseInt(parts2[i].replaceAll("\\D", "")) : 0; + + if (num1 != num2) { + return Integer.compare(num1, num2); + } + } + + return 0; + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to compare versions {} and {}", version1, version2, e); + return 0; + } + } + + /** + * 获取当前Minecraft版本 + */ + private String getCurrentMinecraftVersion() { + try { + // 通过Fabric API获取Minecraft版本 + return net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("minecraft") + .map(container -> container.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown"); + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to get Minecraft version", e); + return "unknown"; + } + } + + /** + * 获取当前PyFabricLoader版本 + */ + private String getCurrentPyFabricVersion() { + try { + // 尝试通过Fabric API获取PyFabricLoader版本 + return net.fabricmc.loader.api.FabricLoader.getInstance().getModContainer("pyfabricloader") + .map(container -> container.getMetadata().getVersion().getFriendlyString()) + .orElse("1.0.0"); // 默认版本 + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Failed to get PyFabricLoader version", e); + return "1.0.0"; + } + } + + public static PythonManager getInstance() { + return INSTANCE; + } + + public void initialize() { + // 创建必要的文件夹 + createDirectories(); + + // 从jar中提取jython到libs目录 + extractJythonFromJar(); + + try { + // 确保libs目录在Python路径中 + String pythonPath = System.getProperty("python.path", + System.getProperty("java.class.path").replace(File.pathSeparatorChar, ':')); + pythonPath += ":" + libsDir.getAbsolutePath(); + System.setProperty("python.path", pythonPath); + + // 设置Jython使用当前线程的上下文类加载器 + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + + // 预加载关键的Jython类以确保它们可用 + PyFabricLoader.LOGGER.info("Preloading critical Jython classes..."); + Class.forName("org.python.core.PyObject"); + Class.forName("org.python.core.PyString"); + Class.forName("org.python.util.PythonInterpreter"); + PyFabricLoader.LOGGER.info("Jython core classes preloaded successfully"); + + // 初始化全局Python解释器 + Properties props = new Properties(); + props.setProperty("python.console.encoding", "UTF-8"); + props.setProperty("python.security.respectJavaAccessibility", "false"); + // 设置使用系统类加载器 + props.setProperty("python.cachedir", new File(libsDir, ".jython-cache").getAbsolutePath()); + // 禁用Jython的finalize钩子以避免访问被限制的java.lang.finalize()方法 + props.setProperty("python.import.site", "false"); + props.setProperty("python.jit", "false"); + props.setProperty("python.security.respectJavaAccessibility", "true"); + + PythonInterpreter.initialize(System.getProperties(), props, new String[0]); + + PyFabricLoader.LOGGER.info("Creating PythonInterpreter instance..."); + globalInterpreter = new PythonInterpreter(); + setupGlobalInterpreter(); + + // 加载所有mod + loadAllMods(); + } catch (ClassNotFoundException e) { + PyFabricLoader.LOGGER.error("Critical Jython class not found: " + e.getMessage(), e); + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error during Jython initialization: " + e.getMessage(), e); + } + } + + private void createDirectories() { + modsDir.mkdirs(); + configsDir.mkdirs(); + libsDir.mkdirs(); + filesDir.mkdirs(); + PyFabricLoader.LOGGER.info("Created directories: mods={}, configs={}, libs={}, files={}", + modsDir.exists(), configsDir.exists(), libsDir.exists(), filesDir.exists()); + } + + private void setupGlobalInterpreter() { + // 设置全局变量 + globalInterpreter.set("__name__", "__main__"); + globalInterpreter.set("ModInfos", new PyDictionary()); + + // 注册PyCommandAPI实例,让Python脚本可以直接访问 + globalInterpreter.set("PyCommandAPI", PyCommandAPI.getInstance()); + + // 注册ConfigManager实例,让Python脚本可以访问配置信息 + globalInterpreter.set("ConfigManager", ConfigManager.getInstance()); + + // 添加libs目录到Python路径,确保jython可以导入pyfabric/libs下的py文件 + try { + String pythonPath = System.getProperty("python.path", + System.getProperty("java.class.path").replace(File.pathSeparatorChar, ':')); + pythonPath += ":" + libsDir.getAbsolutePath(); + System.setProperty("python.path", pythonPath); + + // 直接在解释器中添加路径,确保导入生效 + globalInterpreter.exec("import sys"); + globalInterpreter.exec("sys.path.append('" + libsDir.getAbsolutePath() + "')"); + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Failed to set Python path", e); + } + } + + public void loadAllMods() { + ConfigManager configManager = ConfigManager.getInstance(); + + // 加载ZIP格式的mods + if (configManager.isModeEnabled("Mods")) { + PyFabricLoader.LOGGER.info("Loading mods from ZIP files..."); + String matchingPattern = configManager.getModuleMatchingPattern(); + String priorityPattern = configManager.getPriorityModuleMatchingPattern(); + List customOrder = configManager.getCustomLoadOrder(); + + // 先处理自定义顺序的mods + for (String modName : customOrder) { + File modFile = new File(modsDir, modName); + if (!modFile.exists()) { + modFile = new File(modsDir, modName + ".zip"); + } + if (modFile.exists()) { + loadMod(modFile); + } + } + + // 加载剩余的ZIP mods + File[] zipFiles = modsDir.listFiles((dir, name) -> { + if (!name.toLowerCase().endsWith(".zip")) return false; + // 检查是否已经在自定义顺序中加载过 + if (customOrder.stream().anyMatch(order -> name.equals(order) || name.equals(order + ".zip"))) { + return false; + } + // 应用匹配规则 + return name.matches(matchingPattern) || name.matches(priorityPattern); + }); + + if (zipFiles != null) { + for (File zipFile : zipFiles) { + loadMod(zipFile); + } + } + } + + // 加载单文件模式的mods + if (configManager.isModeEnabled("ModSingleFileMode")) { + PyFabricLoader.LOGGER.info("Loading mods from single Python files..."); + File[] pyFiles = modsDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".py")); + if (pyFiles != null) { + for (File pyFile : pyFiles) { + loadSingleFileMod(pyFile); + } + } + } + } + + private boolean loadSingleFileMod(File pyFile) { + String modId = pyFile.getName().replace(".py", ""); + PyFabricLoader.LOGGER.info("Loading single file mod: {}", modId); + + try { + // 创建新的解释器实例 + PythonInterpreter interpreter = new PythonInterpreter(); + interpreter.set("__name__", modId); + interpreter.set("ModInfos", new PyDictionary()); + interpreter.set("PyCommandAPI", PyCommandAPI.getInstance()); + interpreter.set("ConfigManager", ConfigManager.getInstance()); + + // 添加mods目录到Python路径 + interpreter.exec("import sys"); + interpreter.exec("sys.path.append('" + modsDir.getAbsolutePath() + "')"); + + // 执行Python文件 + interpreter.execfile(pyFile.getAbsolutePath()); + + // 获取ModInfos + PyDictionary modInfos = (PyDictionary) interpreter.get("ModInfos"); + PyModInfo modInfo = new PyModInfo(modId, modInfos); + + // 存储mod信息和解释器 + loadedMods.put(modId, modInfo); + interpreters.put(modId, interpreter); + + PyFabricLoader.LOGGER.info("Successfully loaded single file mod: {} - {}", modId, modInfo.getName()); + return true; + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Failed to load single file mod: {}", modId, e); + return false; + } + } + + public boolean loadMod(File zipFile) { + String modId = zipFile.getName().replace(".zip", ""); + PyFabricLoader.LOGGER.info("Loading mod: {}", modId); + + try { + // 创建临时目录解压zip + Path tempDir = Files.createTempDirectory("pyfabric_mod_"); + extractZip(zipFile, tempDir.toFile()); + + // 查找info.json文件 + File infoJsonFile = new File(tempDir.toFile(), "info.json"); + if (!infoJsonFile.exists()) { + PyFabricLoader.LOGGER.warn("No info.json found in {}, skipping mod", zipFile.getName()); + deleteDirectory(tempDir.toFile()); + return false; + } + + // 读取和解析info.json + Gson gson = new Gson(); + JsonObject infoJson = gson.fromJson(java.nio.file.Files.newBufferedReader(infoJsonFile.toPath()), JsonObject.class); + + // 验证版本要求 + String pyfabricVersion = infoJson.has("pyfabric-version") ? infoJson.get("pyfabric-version").getAsString() : null; + String minecraftVersion = infoJson.has("minecraft-version") ? infoJson.get("minecraft-version").getAsString() : null; + String currentPyFabricVersion = getCurrentPyFabricVersion(); + String currentMinecraftVersion = getCurrentMinecraftVersion(); + + // 验证PyFabricLoader版本 + if (!checkVersion(pyfabricVersion, currentPyFabricVersion)) { + PyFabricLoader.LOGGER.warn("Mod {} requires PyFabricLoader version {} but current is {}, skipping", + modId, pyfabricVersion, currentPyFabricVersion); + deleteDirectory(tempDir.toFile()); + return false; + } + + // 验证Minecraft版本 + if (!checkVersion(minecraftVersion, currentMinecraftVersion)) { + PyFabricLoader.LOGGER.warn("Mod {} requires Minecraft version {} but current is {}, skipping", + modId, minecraftVersion, currentMinecraftVersion); + deleteDirectory(tempDir.toFile()); + return false; + } + + // 查找__init__.py + File initPy = new File(tempDir.toFile(), "__init__.py"); + if (!initPy.exists()) { + PyFabricLoader.LOGGER.warn("No __init__.py found in {}", zipFile.getName()); + deleteDirectory(tempDir.toFile()); + return false; + } + + // 创建新的解释器实例 + PythonInterpreter interpreter = new PythonInterpreter(); + interpreter.set("__name__", modId); + interpreter.set("ModInfos", new PyDictionary()); + + // 添加临时目录到Python路径 + interpreter.exec("import sys"); + interpreter.exec("sys.path.append('" + tempDir.toAbsolutePath() + "')"); + + // 执行__init__.py + interpreter.execfile(initPy.getAbsolutePath()); + + // 从info.json创建PyModInfo + PyModInfo modInfo = new PyModInfo(modId, infoJson); + + // 存储mod信息和解释器 + loadedMods.put(modId, modInfo); + interpreters.put(modId, interpreter); + + PyFabricLoader.LOGGER.info("Successfully loaded mod: {} - {}", modId, modInfo.getName()); + return true; + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Failed to load mod: {}", modId, e); + return false; + } + } + + public boolean reloadMod(String modName) { + // 先移除旧的mod + unloadMod(modName); + + // 查找并加载新的mod + File zipFile = new File(modsDir, modName + ".zip"); + if (!zipFile.exists()) { + // 尝试直接使用文件名 + zipFile = new File(modsDir, modName); + if (!zipFile.exists()) { + return false; + } + } + + return loadMod(zipFile); + } + + public void reloadAllMods() { + // 卸载所有mod + unloadAllMods(); + // 重新加载所有mod + loadAllMods(); + } + + public void unloadMod(String modId) { + if (loadedMods.containsKey(modId)) { + loadedMods.remove(modId); + PythonInterpreter interpreter = interpreters.remove(modId); + if (interpreter != null) { + interpreter.close(); + } + PyFabricLoader.LOGGER.info("Unloaded mod: {}", modId); + } + } + + public void unloadAllMods() { + for (String modId : new ArrayList<>(loadedMods.keySet())) { + unloadMod(modId); + } + } + + public List getLoadedMods() { + return new ArrayList<>(loadedMods.values()); + } + + public String executePython(String code) { + try { + StringWriter writer = new StringWriter(); + globalInterpreter.setOut(writer); + globalInterpreter.setErr(writer); + globalInterpreter.exec(code); + return writer.toString(); + } catch (Exception e) { + return "Error: " + e.getMessage(); + } + } + + public String executePythonFile(String fileName) { + // 修改为执行pyfabric/files下的文件 + File pythonFile = new File(filesDir, fileName); + if (!pythonFile.exists()) { + return "File not found: " + fileName + " in " + filesDir.getAbsolutePath(); + } + + try { + StringWriter writer = new StringWriter(); + globalInterpreter.setOut(writer); + globalInterpreter.setErr(writer); + globalInterpreter.execfile(pythonFile.getAbsolutePath()); + return writer.toString(); + } catch (Exception e) { + return "Error executing " + fileName + ": " + e.getMessage(); + } + } + + private void extractZip(File zipFile, File targetDir) throws IOException { + try (ZipFile zip = new ZipFile(zipFile)) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + File entryFile = new File(targetDir, entry.getName()); + + if (entry.isDirectory()) { + entryFile.mkdirs(); + } else { + // 确保父目录存在 + entryFile.getParentFile().mkdirs(); + // 复制文件内容 + try (java.io.InputStream in = zip.getInputStream(entry); + java.io.OutputStream out = new FileOutputStream(entryFile)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + } + } + } + } + + private void deleteDirectory(File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } + + /** + * 从jar中提取jython相关文件到libs目录 + */ + private void extractJythonFromJar() { + try { + // 检查jython是否已经存在 + File jythonLibDir = new File(libsDir, "Lib"); + if (jythonLibDir.exists() && jythonLibDir.isDirectory() && jythonLibDir.listFiles() != null && jythonLibDir.listFiles().length > 0) { + PyFabricLoader.LOGGER.info("Jython already exists in libs directory, skipping extraction"); + return; + } + + PyFabricLoader.LOGGER.info("Extracting Jython resources to libs directory..."); + + // 确保libs目录存在 + libsDir.mkdirs(); + + // 首先尝试直接从当前jar中提取Jython的Lib目录 + try { + // 使用类加载器直接访问当前jar中的Jython资源 + // 创建一个测试文件来确保目录可写 + File testPy = new File(libsDir, "__init__.py"); + try (FileOutputStream fos = new FileOutputStream(testPy)) { + fos.write("# Jython initialization file\n".getBytes()); + } + + // 直接从项目的libs目录复制Jython jar到pyfabric/libs目录 + File jythonJar = new File("libs/jython-slim-3.0.1-SNAPSHOT-all.jar"); + if (jythonJar.exists()) { + File targetJythonJar = new File(libsDir, "jython-slim-3.0.1-SNAPSHOT-all.jar"); + java.nio.file.Files.copy(jythonJar.toPath(), targetJythonJar.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + PyFabricLoader.LOGGER.info("Copied Jython jar to libs directory"); + + // 从复制的jar中提取Lib目录 + extractJythonFromSpecificJar(targetJythonJar.getAbsolutePath()); + } else { + PyFabricLoader.LOGGER.warn("Jython jar not found in project libs directory"); + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error copying Jython resources", e); + } + + PyFabricLoader.LOGGER.info("Jython resource extraction completed"); + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Failed to extract Jython resources", e); + } + } + + /** + * 查找Jython jar文件的路径 + */ + private String findJythonJarPath() { + try { + // 获取org.python包的类,然后获取其所在的jar路径 + Class pyClass = Class.forName("org.python.core.PyObject"); + String path = pyClass.getProtectionDomain().getCodeSource().getLocation().getPath(); + // 处理URL编码 + return java.net.URLDecoder.decode(path, "UTF-8"); + } catch (Exception e) { + PyFabricLoader.LOGGER.warn("Could not determine Jython jar path", e); + return null; + } + } + + /** + * 从指定的Jython jar中提取文件到libs目录 + */ + private void extractJythonFromSpecificJar(String jarPath) throws IOException { + File jarFile = new File(jarPath); + if (!jarFile.exists()) { + PyFabricLoader.LOGGER.warn("Jython jar file not found: " + jarPath); + return; + } + + try (JarFile jar = new JarFile(jarFile)) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + // 只提取Lib目录下的Python文件 + if (entry.getName().startsWith("Lib/") && !entry.isDirectory()) { + File destFile = new File(libsDir, entry.getName().substring(4)); // 去掉"Lib/"前缀 + destFile.getParentFile().mkdirs(); + + try (java.io.InputStream in = jar.getInputStream(entry); + java.io.OutputStream out = new FileOutputStream(destFile)) { + byte[] buffer = new byte[8192]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } + } + } + } + } + + public static class PyModInfo { + private final String id; + private final String name; + private final String version; + private final String description; + + // 从info.json的JsonObject创建 + public PyModInfo(String id, JsonObject infoJson) { + this.id = id; + this.name = infoJson.has("name") ? infoJson.get("name").getAsString() : id; + this.version = infoJson.has("version") ? infoJson.get("version").getAsString() : "1.0.0"; + this.description = infoJson.has("description") ? infoJson.get("description").getAsString() : ""; + } + + // 兼容旧版从PyDictionary创建的方式 + public PyModInfo(String id, PyDictionary modInfos) { + this.id = id; + this.name = modInfos.get("name") != null ? modInfos.get("name").toString() : id; + this.version = modInfos.get("version") != null ? modInfos.get("version").toString() : "1.0.0"; + this.description = modInfos.get("description") != null ? modInfos.get("description").toString() : ""; + } + + public String getId() { return id; } + public String getName() { return name; } + public String getVersion() { return version; } + public String getDescription() { return description; } + } +} \ No newline at end of file diff --git a/src/main/java/com/gvsds/pyfabricloader/VersionHelper.java b/src/main/java/com/gvsds/pyfabricloader/VersionHelper.java new file mode 100644 index 0000000..8d39981 --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/VersionHelper.java @@ -0,0 +1,120 @@ +package com.gvsds.pyfabricloader; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Minecraft版本检测和兼容性工具类 + */ +public class VersionHelper { + private static final String MINECRAFT_VERSION_CLASS = "net.minecraft.MinecraftVersion"; + private static final String GAME_VERSION_CLASS = "net.minecraft.SharedConstants"; + private static String detectedVersion = null; + + /** + * 检测当前运行的Minecraft版本 + * @return Minecraft版本字符串,如"1.19.2" + */ + public static String getMinecraftVersion() { + if (detectedVersion != null) { + return detectedVersion; + } + + try { + // 尝试通过MinecraftVersion类获取版本(1.17+) + try { + Class minecraftVersionClass = Class.forName(MINECRAFT_VERSION_CLASS); + Object instance = minecraftVersionClass.getMethod("getInstance").invoke(null); + detectedVersion = (String) minecraftVersionClass.getMethod("getName").invoke(instance); + return detectedVersion; + } catch (ClassNotFoundException | NoSuchMethodException e) { + // 尝试通过SharedConstants类获取版本 + try { + Class sharedConstantsClass = Class.forName(GAME_VERSION_CLASS); + Object gameVersion = sharedConstantsClass.getField("getGameVersion").get(null); + detectedVersion = (String) gameVersion.getClass().getMethod("getName").invoke(gameVersion); + return detectedVersion; + } catch (Exception ex) { + // 尝试直接获取版本字符串 + try { + Class sharedConstantsClass = Class.forName(GAME_VERSION_CLASS); + detectedVersion = (String) sharedConstantsClass.getField("VERSION_STRING").get(null); + return detectedVersion; + } catch (Exception exc) { + PyFabricLoader.LOGGER.warn("Failed to detect Minecraft version"); + detectedVersion = "unknown"; + } + } + } + } catch (Exception e) { + PyFabricLoader.LOGGER.error("Error detecting Minecraft version: {}", e.getMessage(), e); + } + + return detectedVersion; + } + + /** + * 比较两个Minecraft版本 + * @param version1 第一个版本 + * @param version2 第二个版本 + * @return 如果version1大于version2返回正数,如果小于返回负数,如果等于返回0 + */ + public static int compareVersions(String version1, String version2) { + if (version1 == null || version2 == null) { + return 0; + } + + // 解析版本号(如1.19.2 -> [1, 19, 2]) + int[] parts1 = parseVersion(version1); + int[] parts2 = parseVersion(version2); + + for (int i = 0; i < Math.max(parts1.length, parts2.length); i++) { + int part1 = i < parts1.length ? parts1[i] : 0; + int part2 = i < parts2.length ? parts2[i] : 0; + + if (part1 != part2) { + return part1 - part2; + } + } + + return 0; + } + + /** + * 解析版本字符串为整数数组 + */ + private static int[] parseVersion(String version) { + Pattern pattern = Pattern.compile("\\d+"); + Matcher matcher = pattern.matcher(version); + + int count = 0; + Matcher countMatcher = pattern.matcher(version); + while (countMatcher.find()) { + count++; + } + + int[] parts = new int[count]; + int index = 0; + while (matcher.find()) { + parts[index++] = Integer.parseInt(matcher.group()); + } + + return parts; + } + + /** + * 检查当前版本是否大于或等于指定版本 + */ + public static boolean isVersionAtLeast(String version) { + return compareVersions(getMinecraftVersion(), version) >= 0; + } + + /** + * 获取当前版本对应的实现类后缀 + * @return 版本后缀,如"1_19_2" + */ + public static String getVersionSuffix() { + String version = getMinecraftVersion(); + return version.replace('.', '_').replaceFirst("^([0-9]+_[0-9]+)(_[0-9]+)?.*$", "$1"); + } +} \ No newline at end of file diff --git a/src/main/java/com/gvsds/pyfabricloader/mixin/ExampleMixin.java b/src/main/java/com/gvsds/pyfabricloader/mixin/ExampleMixin.java new file mode 100644 index 0000000..b296f14 --- /dev/null +++ b/src/main/java/com/gvsds/pyfabricloader/mixin/ExampleMixin.java @@ -0,0 +1,15 @@ +package com.gvsds.pyfabricloader.mixin; + +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftServer.class) +public class ExampleMixin { + @Inject(at = @At("HEAD"), method = "loadWorld") + private void init(CallbackInfo info) { + // This code is injected into the start of MinecraftServer.loadWorld()V + } +} \ No newline at end of file diff --git a/src/main/resources/assets/pyfabricloader/icon.png b/src/main/resources/assets/pyfabricloader/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3b0e792ccbb103528fc0b65fea8c6c017822a75b GIT binary patch literal 3168 zcmZ`+`#;nF7k_U?8_6h_gt^Ui<&ub)C6|#XLR#jsMak3$nY-}`A*>LqMTyo$u4RPc z)66Z26myGDxnCn23E%hU5BUD@x;$Rzb)M(E&UwAg>zvfnPIls=@}d9$h}+|>UBHp{ zUxSN){d=r$Cjji0wzochHUcuwpgxsum2K_L_m5gS<$`x6il!nG&i0$7?Qd)Rk^_w{j2-ks5XcOC+h{JJA{Ozg-hp<>+KLat**B5up22P=IpPOF5+V#^k_%MQ z5%wULdP3&J6DegGLzT-Qp!%oE;j>VVm4^C)1C6%JtRUKf76BJ-@J1k)f!0uG1XAJ= zO`r>bDvd=fYD4~2*|!{r#x)5kbX%Zt<~N|DAFC2U7;|e3gUW9^(mrpMK^^Vi3nr`O=C7wO8E>2ssT#F<5uTv-H~I1udh@^{Pf#c>-U~$ zMaAb-2Xs$%ek(bd*YP+Dt3%@6YVTin+%s5DEjJ46!xQ7Tu6w=?bnA6YMFOYaZSe^P z+~IJGpPaoeFV7$0v9A_Rr7jhB=kIj)RFYb2&Gd*3T3MFcwF+io_%dRnya(133T%!2 zEisy$B6~mkqEJ-uaIK1j#V=%*jL;^%xDXD>_u2llVHi5;Shu-YUt*mqB{Scdrzf^Y zLdpEx+nw!bUSl+y^{mI_>Zm!xm~!un$J?05X342U<%5Kp9H_9W2B+^Nk{Gvfu|A3< z8ab_Xp|*l)cHJROR&Vk6`5&L1EptD!oKN02rj*9ig-itTFAu(Y7VF%fWxzc|bH;9{ z&$x~zZ*MNW)6;8MP8_Cwiw)`bqPANIhSl6&xaZzGa;1a6FjR7-zpNlbpW9}KiY2ev zNT@!|IA9{P*b^HUwM!_b>4{)8TJJF*yV9nXf4gta82Y1RM2_TP8C$Asm}sDJN1o-# zu*y>~k2p**1qtdxr^sz|l@}KK2Fu4QTYZ{c*KsY@3u6A#bk1L)d8O=dd@K1oh|yY<1Yy8GJWkO zA(7p5%bh>jc9Gi~Qx|_t_pEg4y~7I@M<@iMC2ff1j5;aDik}~-tZ8HD`zDS={a%~X zTo_`%YdJ?Yp$w8!&y*7_?lZLJw|~#+5ek`>@x+3d6?xKQZd_vppdCv8F zxatQzDN0lFmT?Vz&s^0?Q<35hRZC3aa#D+8n~`x1xTAK;D23tt%I<<HpQot-|aw^^m(Bq5J;Tx0l6RPfzk1BCFN>maaCg-35FY zh8JNgw=+OqRacf%E~_wZFxX@|UNAzPm@16FJN`a}KtRQ<`_{9n|9M$7T=+gkLhL#| zTjLbQ6E9#I(z_O%)-55;S3Ld>ZDr~LoU9j$VOPxRAZ1EA3u<`9eXR1@Gh>=XLT1U26dhMeB?Fmy?R0_(*nIR#a?Blg{oZMt41lhT43QIHR1Rmfim#_tapB z;Pz$P40&|vH9W^5q}-idy1C!0G=h4+e@aaD&_Lge8@6D$JzJAv3=?Bx><03PQTFFH z6iDAU9G2m#!nK4faQ~)YiOmx?wD$g3Gq050AL!66&eCWuV>)9T}$d;Lvu#}?(FfS|mE~yH6udchl`jD`W5v8U5vE2N8 zR3I_tr%iO)^6VTK3H+Qq_1XE+v(H%M@qdyMC4!s1>ciVXSC5=BDBRf#`7}$0xEMrB zOCMcGy->mUwGPHOUaZ;eN-#!Aiz@NY3y${IZd#NFynSAo!k_24W}+;WBz;ek%!f`D z|J);SE2%aJjl3ODBNG5;Bz)<3P9~&J>1s*`YSoNSkC~Y@-;wbtca3>LS?RT~9oA6tt?#iA`fWeO=*pp38#rzP@%UWQ zyz7;(rIHFo9pMsfbBr%`x$TVMneLOQO&{WKWpVF*Eq6G1?(55i6wS9BNj!60!Q77n zC;T#yB{}3BqGSx4(xiJ9UAPCVy$=rapuE5DTt?<^>T&sL} zxN1mmvxxr2cnT`0D@mfyZu-fhs#s`4Npewb@TZ3fXz`Vp+7n8p6{rIv9yb6v6H9@s z{y&Riq!FQ&D1RG+{dyLJ3(${xk} zQD;k!%w68vfQlxgySX@sS0$s?CT9OB>E*zO3vGtUmFpQhua3#e>2pC-j|DsaU`UGq z@4zoy_5Inen(!a#z#liHO!ZF1igujn#3Bp(Q+uDkdS`o#BrU8}iKA(|UBxXRedsb` zV&0#1jzYG{LXP?^Pjod-6}Mh^fad*&&~9wCc5x4wD^6AjT0S@c-ZxZ2ZM7hVDUEvm z?$CAB-HgLl2)gO~>XFc@UD}X48JF(PxI)REO?N0|Qp&uW{tPS{cUKt0o7DM#L>R`+ zUkBon6m)mm4NA6u=yDOv``p_6Q;8raOy~a+Y28A(fvY*dGuwfw-?;!*;`H+_S7@=k zZ{eH=bln{H%-9)054;gu=8YIz1xg0Z(YWW5{JJetP<0M4V>QTH)q=#x=Z7&$ zV0hVH>;pT**ypC=P7E6NhR&ZccK`}SG`#$^A-zejoTtt}*D?At#&!sLFelcyP^u{a zV$U{ppoOU|R&V40STX#Yt~1c$1Ai5Ev0%%dxLyjF4vyU148npUp?9%>NSLatKWO5l2Ioo8AV-mmg6(*)JlNUE idSOrmPDK7I5QG57Vx_<)!N-r_3Se*JWL;+EllVWn-Q=AB literal 0 HcmV?d00001 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..0a8724e --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "id": "pyfabricloader", + "version": "${version}", + "name": "PyFabricLoader", + "description": "A Python mod loader for Minecraft Fabric using Jython, allowing to load and run Python mods.", + "authors": [ + "银河万通软件开发工作室 工程部 TermiNexus" + ], + "contact": { + "homepage": "https://www.gvsds.com", + "sources": "https://www.gvsds.com" + }, + "license": "MIT", + "icon": "assets/pyfabricloader/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.gvsds.pyfabricloader.PyFabricLoader" + ] + }, + "mixins": [ + "pyfabricloader.mixins.json", + { + "config": "pyfabricloader.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.14.0", + "minecraft": "[1.18.1,1.21.10]", + "java": ">=17", + "fabric-api": "*" + }, + "suggests": { + "another-mod": "*" + } +} \ No newline at end of file diff --git a/src/main/resources/lang/en.json b/src/main/resources/lang/en.json new file mode 100644 index 0000000..a8694c2 --- /dev/null +++ b/src/main/resources/lang/en.json @@ -0,0 +1,43 @@ +{ + "commands": { + "help": { + "header": "PyFabricLoader Command Help", + "main": "Show help information", + "list": "Show currently loaded mods", + "reload": "Force reload all mods (admin)", + "reload_file": "Force reload specific mod (admin)", + "exec": "Execute Jython code", + "run": "Execute Python file under pyfabric/files", + "help": "Show command help", + "about": "Show about information", + "lang": "Switch language" + } + }, + "messages": { + "no_mods": "No Python modules loaded currently.", + "loaded_mods": "Loaded Python modules (%d):", + "reload_success": "Successfully reloaded all Python modules!", + "reload_failed": "Error during module reload: %s", + "mod_reloaded": "Successfully reloaded module: %s", + "mod_not_found": "Cannot reload module: %s, file may not exist.", + "mod_reload_failed": "Error reloading module %s: %s", + "execution_success_no_output": "Code executed successfully with no output.", + "execution_result": "Python execution result:", + "execution_error": "Python execution error: %s", + "file_execution_result": "Execution result of %s:", + "file_execution_failed": "Error executing file %s: %s", + "about": { + "header": "PyFabricLoader Version %s", + "author": "Author: %s", + "bilibili": "Bilibili Account:", + "website": "Studio Website:", + "thanks": "Thank you for using!" + }, + "language_changed": "Language changed to: %s", + "language_invalid": "Invalid language code: %s, supported languages: zh-CN, zh-TW, en", + "language_change_failed": "Failed to change language: %s", + "mod_loaded": "Module loaded: %s", + "load_failed": "Load failed: %s", + "init_failed": "Initialization failed: %s" + } +} \ No newline at end of file diff --git a/src/main/resources/lang/zh-CN.json b/src/main/resources/lang/zh-CN.json new file mode 100644 index 0000000..02053b8 --- /dev/null +++ b/src/main/resources/lang/zh-CN.json @@ -0,0 +1,43 @@ +{ + "commands": { + "help": { + "header": "PyFabricLoader 命令帮助", + "main": "显示帮助信息", + "list": "显示当前加载的mod", + "reload": "强制重载所有mod(管理员)", + "reload_file": "强制重载指定mod(管理员)", + "exec": "执行Jython代码", + "run": "执行pyfabric/files下的Python文件", + "help": "显示命令帮助", + "about": "显示关于信息", + "lang": "切换语言" + } + }, + "messages": { + "no_mods": "当前没有加载任何Python模组。", + "loaded_mods": "已加载的Python模组(%d):", + "reload_success": "已成功重载所有Python模组!", + "reload_failed": "重载模组时出错:%s", + "mod_reloaded": "已成功重载模组:%s", + "mod_not_found": "无法重载模组:%s,文件可能不存在。", + "mod_reload_failed": "重载模组 %s 时出错:%s", + "execution_success_no_output": "代码执行成功,但没有输出。", + "execution_result": "Python 执行结果:", + "execution_error": "Python 执行错误:%s", + "file_execution_result": "执行 %s 结果:", + "file_execution_failed": "执行文件 %s 时出错:%s", + "about": { + "header": "PyFabricLoader Version %s", + "author": "作者:%s", + "bilibili": "B站账号:", + "website": "工作室官网:", + "thanks": "感谢您的使用!" + }, + "language_changed": "语言已切换为:%s", + "language_invalid": "无效的语言代码:%s,支持的语言:zh-CN, zh-TW, en", + "language_change_failed": "切换语言失败:%s", + "mod_loaded": "模组已加载: %s", + "load_failed": "加载失败: %s", + "init_failed": "初始化失败: %s" + } +} \ No newline at end of file diff --git a/src/main/resources/lang/zh-TW.json b/src/main/resources/lang/zh-TW.json new file mode 100644 index 0000000..f01608b --- /dev/null +++ b/src/main/resources/lang/zh-TW.json @@ -0,0 +1,43 @@ +{ + "commands": { + "help": { + "header": "PyFabricLoader 命令幫助", + "main": "顯示幫助資訊", + "list": "顯示當前加載的mod", + "reload": "強制重新加載所有mod(管理員)", + "reload_file": "強制重新加載指定mod(管理員)", + "exec": "執行Jython代碼", + "run": "執行pyfabric/files下的Python文件", + "help": "顯示命令幫助", + "about": "顯示關於資訊", + "lang": "切換語言" + } + }, + "messages": { + "no_mods": "當前沒有加載任何Python模組。", + "loaded_mods": "已加載的Python模組(%d):", + "reload_success": "已成功重新加載所有Python模組!", + "reload_failed": "重新加載模組時出錯:%s", + "mod_reloaded": "已成功重新加載模組:%s", + "mod_not_found": "無法重新加載模組:%s,文件可能不存在。", + "mod_reload_failed": "重新加載模組 %s 時出錯:%s", + "execution_success_no_output": "代碼執行成功,但沒有輸出。", + "execution_result": "Python 執行結果:", + "execution_error": "Python 執行錯誤:%s", + "file_execution_result": "執行 %s 結果:", + "file_execution_failed": "執行文件 %s 時出錯:%s", + "about": { + "header": "PyFabricLoader Version %s", + "author": "作者:%s", + "bilibili": "B站賬號:", + "website": "工作室官網:", + "thanks": "感謝您的使用!" + }, + "language_changed": "語言已切換為:%s", + "language_invalid": "無效的語言代碼:%s,支援的語言:zh-CN, zh-TW, en", + "language_change_failed": "切換語言失敗:%s", + "mod_loaded": "模組已加載: %s", + "load_failed": "加載失敗: %s", + "init_failed": "初始化失敗: %s" + } +} \ No newline at end of file diff --git a/src/main/resources/loader.json b/src/main/resources/loader.json new file mode 100644 index 0000000..9c02a4b --- /dev/null +++ b/src/main/resources/loader.json @@ -0,0 +1,14 @@ +{ + "Mode": { + "Mods": true, + "ModSingleFileMode": true, + "Libs": true, + "Files": true + }, + "Preload": { + "ModuleMatching": ".*\\.zip$", + "PriorityModuleMatching": "^!.*\\.zip$", + "CustomLoadOrder": [] + }, + "Lang": "zh-CN" +} \ No newline at end of file diff --git a/src/main/resources/pyfabricloader.mixins.json b/src/main/resources/pyfabricloader.mixins.json new file mode 100644 index 0000000..f1b87fc --- /dev/null +++ b/src/main/resources/pyfabricloader.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "package": "com.gvsds.pyfabricloader.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "ExampleMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + } +} \ No newline at end of file