The Fisrt Updated
This commit is contained in:
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal 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
30
.github/workflows/build.yml
vendored
Normal 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
58
.gitignore
vendored
Normal 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
|
||||
*~
|
||||
121
LICENSE
Normal file
121
LICENSE
Normal 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
38
META-INF/fabric.mod.json
Normal 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
1
PyFabric/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
import command
|
||||
0
PyFabric/command.py
Normal file
0
PyFabric/command.py
Normal file
94
__init__.py
Normal file
94
__init__.py
Normal 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
390
build.gradle
Normal 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
114
create_version_jars.py
Normal 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
46
gradle.properties
Normal 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
|
||||
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
248
gradlew
vendored
Normal 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
93
gradlew.bat
vendored
Normal 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
7
info.json
Normal 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
48
loader.json
Normal 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
10
settings.gradle
Normal file
@@ -0,0 +1,10 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'Fabric'
|
||||
url = 'https://maven.fabricmc.net/'
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
11
src/client/resources/pyfabricloader.client.mixins.json
Normal file
11
src/client/resources/pyfabricloader.client.mixins.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.gvsds.pyfabricloader.mixin.client",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"ExampleClientMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
189
src/main/java/com/gvsds/pyfabricloader/CommandHandler.java
Normal file
189
src/main/java/com/gvsds/pyfabricloader/CommandHandler.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
469
src/main/java/com/gvsds/pyfabricloader/ConfigManager.java
Normal file
469
src/main/java/com/gvsds/pyfabricloader/ConfigManager.java
Normal 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$");
|
||||
}
|
||||
}
|
||||
430
src/main/java/com/gvsds/pyfabricloader/PyCommandAPI.java
Normal file
430
src/main/java/com/gvsds/pyfabricloader/PyCommandAPI.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/gvsds/pyfabricloader/PyFabricLoader.java
Normal file
37
src/main/java/com/gvsds/pyfabricloader/PyFabricLoader.java
Normal 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!");
|
||||
}
|
||||
}
|
||||
634
src/main/java/com/gvsds/pyfabricloader/PythonManager.java
Normal file
634
src/main/java/com/gvsds/pyfabricloader/PythonManager.java
Normal 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; }
|
||||
}
|
||||
}
|
||||
120
src/main/java/com/gvsds/pyfabricloader/VersionHelper.java
Normal file
120
src/main/java/com/gvsds/pyfabricloader/VersionHelper.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/assets/pyfabricloader/icon.png
Normal file
BIN
src/main/resources/assets/pyfabricloader/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
38
src/main/resources/fabric.mod.json
Normal file
38
src/main/resources/fabric.mod.json
Normal 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": "*"
|
||||
}
|
||||
}
|
||||
43
src/main/resources/lang/en.json
Normal file
43
src/main/resources/lang/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
43
src/main/resources/lang/zh-CN.json
Normal file
43
src/main/resources/lang/zh-CN.json
Normal 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"
|
||||
}
|
||||
}
|
||||
43
src/main/resources/lang/zh-TW.json
Normal file
43
src/main/resources/lang/zh-TW.json
Normal 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"
|
||||
}
|
||||
}
|
||||
14
src/main/resources/loader.json
Normal file
14
src/main/resources/loader.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Mode": {
|
||||
"Mods": true,
|
||||
"ModSingleFileMode": true,
|
||||
"Libs": true,
|
||||
"Files": true
|
||||
},
|
||||
"Preload": {
|
||||
"ModuleMatching": ".*\\.zip$",
|
||||
"PriorityModuleMatching": "^!.*\\.zip$",
|
||||
"CustomLoadOrder": []
|
||||
},
|
||||
"Lang": "zh-CN"
|
||||
}
|
||||
14
src/main/resources/pyfabricloader.mixins.json
Normal file
14
src/main/resources/pyfabricloader.mixins.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.gvsds.pyfabricloader.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
"ExampleMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"overwrites": {
|
||||
"requireAnnotations": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user