The Fisrt Updated

This commit is contained in:
2026-01-25 13:55:03 +08:00
commit bb5dd599d8
35 changed files with 3439 additions and 0 deletions

9
.gitattributes vendored Normal file
View File

@@ -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

30
.github/workflows/build.yml vendored Normal file
View File

@@ -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/

58
.gitignore vendored Normal file
View File

@@ -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
*~

0
1.py Normal file
View File

121
LICENSE Normal file
View File

@@ -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.

38
META-INF/fabric.mod.json Normal file
View File

@@ -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": "*"
}
}

1
PyFabric/__init__.py Normal file
View File

@@ -0,0 +1 @@
import command

0
PyFabric/command.py Normal file
View File

94
__init__.py Normal file
View File

@@ -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 <player> title <message>")
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 <player> title <message>")
command_api.registerSimpleCommand("simple-test", ExecuteSimpleTest)
print("Successfully registered /simple-test command")
print("TestTitleModule initializing...")
RegisterCommands()
print("TestTitleModule initialization complete!")

390
build.gradle Normal file
View File

@@ -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.
}
}

114
create_version_jars.py Normal file
View File

@@ -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())

46
gradle.properties Normal file
View File

@@ -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

View File

@@ -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

248
gradlew vendored Normal file
View File

@@ -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" "$@"

93
gradlew.bat vendored Normal file
View File

@@ -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

7
info.json Normal file
View File

@@ -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"
}

48
loader.json Normal file
View File

@@ -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

10
settings.gradle Normal file
View File

@@ -0,0 +1,10 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}

View File

@@ -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.
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,11 @@
{
"required": true,
"package": "com.gvsds.pyfabricloader.mixin.client",
"compatibilityLevel": "JAVA_21",
"client": [
"ExampleClientMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -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<ServerCommandSource> 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<PythonManager.PyModInfo> 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<Text>, boolean))
Method sendFeedbackMethod = ServerCommandSource.class.getMethod("sendFeedback", Supplier.class, boolean.class);
sendFeedbackMethod.invoke(source, (Supplier<Text>) () -> 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);
}
}
}

View File

@@ -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<String, JsonObject> 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<String, JsonElement> 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<Text>
Method sendFeedbackMethod = ServerCommandSource.class.getMethod("sendFeedback", java.util.function.Supplier.class, boolean.class);
sendFeedbackMethod.invoke(source, (java.util.function.Supplier<Text>)() -> 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<String> getCustomLoadOrder() {
List<String> 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<String, JsonObject> 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$");
}
}

View File

@@ -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<ServerCommandSource> 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<ServerCommandSource> 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<Text> 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<ServerCommandSource> 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
}
}
}

View File

@@ -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!");
}
}

View File

@@ -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<String, PyModInfo> loadedMods = new HashMap<>();
private final Map<String, PythonInterpreter> 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<String> 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<PyModInfo> 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<? extends ZipEntry> 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<JarEntry> 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; }
}
}

View File

@@ -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");
}
}

View File

@@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -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": "*"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,14 @@
{
"Mode": {
"Mods": true,
"ModSingleFileMode": true,
"Libs": true,
"Files": true
},
"Preload": {
"ModuleMatching": ".*\\.zip$",
"PriorityModuleMatching": "^!.*\\.zip$",
"CustomLoadOrder": []
},
"Lang": "zh-CN"
}

View File

@@ -0,0 +1,14 @@
{
"required": true,
"package": "com.gvsds.pyfabricloader.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"ExampleMixin"
],
"injectors": {
"defaultRequire": 1
},
"overwrites": {
"requireAnnotations": true
}
}