半成品
This commit is contained in:
parent
badaf12bf5
commit
62204064e9
106
README.md
Normal file
106
README.md
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Scrafitipty(暂定名称) - 脚本执行器
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
|
Scrafitipty(暂定名称) 是一个 Minecraft Fabric Mod,允许玩家在游戏中运行自定义脚本来自动化任务、扩展游戏功能。它提供了一个类似 Python 的脚本语言解释器,让玩家可以编写简单的脚本来控制游戏行为。
|
||||||
|
|
||||||
|
> **重要提示**
|
||||||
|
> 此 Mod 仍处于**半成品阶段**,功能有限且可能存在不稳定性。脚本语法只是**表面上类似 Python**,但**并未使用真正的 Python 解释器**。
|
||||||
|
|
||||||
|
### 基本命令
|
||||||
|
|
||||||
|
| 命令 | 描述 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/scrafitipty run <脚本名>` | 运行指定脚本 | `/scrafitipty run example_script` |
|
||||||
|
| `/scrafitipty open <脚本名>` | 打开脚本文件 | `/scrafitipty open example_script` |
|
||||||
|
| `/scrafitipty open_folder` | 打开脚本文件夹 | `/scrafitipty open_folder` |
|
||||||
|
| `/scrafitipty list` | 列出所有可用脚本 | `/scrafitipty list` |
|
||||||
|
| `/scrafitipty delete <脚本名>` | 删除脚本 | `/scrafitipty delete old_script` |
|
||||||
|
|
||||||
|
首次使用时,系统会自动在游戏目录下创建 `Scrafitipty_scripts` 文件夹,并生成一个示例脚本 `example_script.py`。
|
||||||
|
|
||||||
|
### 脚本位置
|
||||||
|
所有脚本都存储在 Minecraft 根目录下的 `Scrafitipty_scripts` 文件夹中:
|
||||||
|
```
|
||||||
|
.minecraft/
|
||||||
|
└── Scrafitipty_scripts/
|
||||||
|
├── example_script.py
|
||||||
|
└── your_scripts_here.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基本语法
|
||||||
|
|
||||||
|
Scrafitipty 使用类似 Python 的语法,但请注意:
|
||||||
|
- **不是真正的 Python** - 这是一个简化版的自定义脚本语言
|
||||||
|
- 支持变量、条件语句(`if`/`else`)、循环(`while`)
|
||||||
|
- 使用缩进(4个空格)表示代码块
|
||||||
|
- 支持单行注释(以 `#` 开头)
|
||||||
|
|
||||||
|
### 内置函数
|
||||||
|
|
||||||
|
#### 游戏交互
|
||||||
|
|
||||||
|
| 函数 | 参数 | 描述 | 示例 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `send_chat(message)` | `message`: 字符串 | 发送聊天消息 | `send_chat("Hello Minecraft!")` |
|
||||||
|
| `send_command(command)` | `command`: 字符串 | 执行游戏命令 | `send_command("time set day")` |
|
||||||
|
| `print_output(message)` | `message`: 字符串 | 显示仅自己可见的消息 | `print_output("脚本运行中...")` |
|
||||||
|
| `get_player_x()` | 无 | 获取玩家 X 坐标 | `x = get_player_x()` |
|
||||||
|
| `get_player_y()` | 无 | 获取玩家 Y 坐标 | `y = get_player_y()` |
|
||||||
|
| `get_player_z()` | 无 | 获取玩家 Z 坐标 | `z = get_player_z()` |
|
||||||
|
| `get_player_dimension()` | 无 | 获取玩家当前维度 | `dim = get_player_dimension()` |
|
||||||
|
| `get_time()` | 无 | 获取游戏时间 | `time = get_time()` |
|
||||||
|
| `get_block_id(x, y, z)` | `x, y, z`: 数字 | 获取指定位置的方块ID | `block = get_block_id(0, 64, 0)` |
|
||||||
|
| `crash_game()` | 无 | 崩溃游戏(测试用) | `crash_game()` |
|
||||||
|
| `sleep(milliseconds)` | `milliseconds`: 数字 | 延迟执行 | `sleep(2000) # 等待2秒` |
|
||||||
|
|
||||||
|
#### 实用功能
|
||||||
|
|
||||||
|
| 函数 | 参数 | 描述 | 示例 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `get_random_number(min, max)` | `min, max`: 数字 | 生成随机整数 | `rand = get_random_number(1, 100)` |
|
||||||
|
| `execute_system_command(command)` | `command`: 字符串 | 执行系统命令(**危险!**) | `execute_system_command("notepad.exe")` |
|
||||||
|
|
||||||
|
### 数据类型
|
||||||
|
|
||||||
|
- **数字**:整数和浮点数(`123`, `3.14`)
|
||||||
|
- **字符串**:用双引号包围(`"Hello"`)
|
||||||
|
- **布尔值**:`true` 和 `false`
|
||||||
|
- **变量**:动态类型,无需声明类型
|
||||||
|
|
||||||
|
### 控制结构
|
||||||
|
|
||||||
|
#### 条件语句
|
||||||
|
```python
|
||||||
|
if condition:
|
||||||
|
# 条件为真时执行
|
||||||
|
else:
|
||||||
|
# 条件为假时执行
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 循环
|
||||||
|
```python
|
||||||
|
count = 0
|
||||||
|
while count < 5:
|
||||||
|
print_output("计数: " + str(count))
|
||||||
|
count = count + 1
|
||||||
|
sleep(500)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运算符
|
||||||
|
|
||||||
|
| 类型 | 运算符 | 示例 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 算术 | `+`, `-`, `*`, `/` | `result = 10 + 5` |
|
||||||
|
| 比较 | `==`, `!=`, `<`, `>`, `<=`, `>=` | `if x > 10: ...` |
|
||||||
|
| 逻辑 | `and`, `or`, `not` | `if a and b: ...` |
|
||||||
|
|
||||||
|
### 类型转换
|
||||||
|
```python
|
||||||
|
# 转换为字符串
|
||||||
|
str_value = str(123) # "123"
|
||||||
|
|
||||||
|
# 字符串转数字(需要手动实现)
|
||||||
|
# 当前版本暂不支持自动转换
|
||||||
|
```
|
||||||
|
|
@ -26,6 +26,8 @@ dependencies {
|
|||||||
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||||
|
|
||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||||
|
|
||||||
|
implementation 'org.mozilla:rhino:1.7.14'
|
||||||
}
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
|
@ -6,7 +6,7 @@ minecraft_version=1.21.4
|
|||||||
yarn_mappings=1.21.4+build.8
|
yarn_mappings=1.21.4+build.8
|
||||||
loader_version=0.16.10
|
loader_version=0.16.10
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=1.14.514.001
|
mod_version=1.14.514.005
|
||||||
maven_group=org.branulf
|
maven_group=org.branulf
|
||||||
archives_base_name=scrafitipty
|
archives_base_name=scrafitipty
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
package org.branulf.scrafitipty;
|
|
||||||
|
|
||||||
import net.minecraft.client.MinecraftClient;
|
|
||||||
import net.minecraft.client.gui.screen.ChatScreen;
|
|
||||||
import net.minecraft.text.Text;
|
|
||||||
|
|
||||||
public class BuiltInFunctions {
|
|
||||||
public static void execute(String function, String args) {
|
|
||||||
switch (function.toLowerCase()) {
|
|
||||||
case "chat" -> sendChat(args.replace("\"", ""));
|
|
||||||
case "command" -> sendCommand(args.replace("\"", ""));
|
|
||||||
case "sleep" -> sleep(Integer.parseInt(args));
|
|
||||||
case "opengui" -> MinecraftClient.getInstance().setScreen(new ChatScreen(""));
|
|
||||||
case "crash" -> throw new RuntimeException("Script forced crash");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendChat(String message) {
|
|
||||||
MinecraftClient client = MinecraftClient.getInstance();
|
|
||||||
if (client.player != null) {
|
|
||||||
client.player.networkHandler.sendChatMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sendCommand(String command) {
|
|
||||||
MinecraftClient client = MinecraftClient.getInstance();
|
|
||||||
if (client.player != null) {
|
|
||||||
client.player.networkHandler.sendChatCommand(command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sleep(int milliseconds) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(milliseconds);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,13 +2,38 @@ package org.branulf.scrafitipty;
|
|||||||
|
|
||||||
import net.fabricmc.api.ClientModInitializer;
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
|
||||||
public class Scrafitipty implements ClientModInitializer {
|
public class Scrafitipty implements ClientModInitializer {
|
||||||
|
public static final String MOD_ID = "scrafitipty";
|
||||||
|
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||||
|
public static Path SCRIPT_DIR;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
||||||
public void onInitializeClient() {
|
public void onInitializeClient() {
|
||||||
ScriptFileManager.setupScriptsDir();
|
LOGGER.info("Scrafitipty Mod 初始化中...");
|
||||||
ClientCommandRegistrationCallback.EVENT.register(
|
|
||||||
(dispatcher, registryAccess) -> ScriptCommand.register(dispatcher)
|
SCRIPT_DIR = MinecraftClient.getInstance().runDirectory.toPath().resolve("Scrafitipty_scripts");
|
||||||
);
|
try {
|
||||||
|
if (Files.notExists(SCRIPT_DIR)) {
|
||||||
|
Files.createDirectories(SCRIPT_DIR);
|
||||||
|
LOGGER.info("创建脚本目录: {}", SCRIPT_DIR);
|
||||||
|
}
|
||||||
|
ScriptManager.createExampleScript();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("无法创建脚本目录或示例脚本: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientCommandRegistrationCallback.EVENT.register(ScrafitiptyCommands::register);
|
||||||
|
|
||||||
|
LOGGER.info("Scrafitipty Mod 初始化完成。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
234
src/main/java/org/branulf/scrafitipty/ScrafitiptyCommands.java
Normal file
234
src/main/java/org/branulf/scrafitipty/ScrafitiptyCommands.java
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package org.branulf.scrafitipty;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.command.CommandRegistryAccess;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
|
||||||
|
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
|
||||||
|
|
||||||
|
public class ScrafitiptyCommands {
|
||||||
|
|
||||||
|
|
||||||
|
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
|
||||||
|
|
||||||
|
dispatcher.register(literal("scrafitipty")
|
||||||
|
.then(literal("run")
|
||||||
|
.then(argument("script_name", StringArgumentType.string())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
try (Stream<Path> paths = Files.list(Scrafitipty.SCRIPT_DIR)) {
|
||||||
|
paths.filter(Files::isRegularFile)
|
||||||
|
.filter(p -> p.toString().endsWith(".py"))
|
||||||
|
.map(p -> p.getFileName().toString().replace(".py", ""))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Scrafitipty.LOGGER.warn("无法列出脚本进行自动补全: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(ScrafitiptyCommands::runScript)))
|
||||||
|
.then(literal("open")
|
||||||
|
.then(argument("script_name", StringArgumentType.string())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
try (Stream<Path> paths = Files.list(Scrafitipty.SCRIPT_DIR)) {
|
||||||
|
paths.filter(Files::isRegularFile)
|
||||||
|
.filter(p -> p.toString().endsWith(".py"))
|
||||||
|
.map(p -> p.getFileName().toString().replace(".py", ""))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Scrafitipty.LOGGER.warn("无法列出脚本进行自动补全: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(ScrafitiptyCommands::openScript)))
|
||||||
|
.then(literal("delete")
|
||||||
|
.then(argument("script_name", StringArgumentType.string())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
try (Stream<Path> paths = Files.list(Scrafitipty.SCRIPT_DIR)) {
|
||||||
|
paths.filter(Files::isRegularFile)
|
||||||
|
.filter(p -> p.toString().endsWith(".py"))
|
||||||
|
.map(p -> p.getFileName().toString().replace(".py", ""))
|
||||||
|
.forEach(builder::suggest);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Scrafitipty.LOGGER.warn("无法列出脚本进行自动补全: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(ScrafitiptyCommands::deleteScript)))
|
||||||
|
.then(literal("list")
|
||||||
|
.executes(ScrafitiptyCommands::listScripts))
|
||||||
|
.then(literal("open_folder")
|
||||||
|
.executes(ScrafitiptyCommands::openScriptFolder))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int runScript(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
|
||||||
|
String scriptName = StringArgumentType.getString(context, "script_name");
|
||||||
|
Path scriptPath = Scrafitipty.SCRIPT_DIR.resolve(scriptName + ".py");
|
||||||
|
|
||||||
|
if (MinecraftClient.getInstance().player == null) {
|
||||||
|
context.getSource().sendError(Text.translatable("scrafitipty.command.error.not_in_game"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.notExists(scriptPath)) {
|
||||||
|
context.getSource().sendError(Text.translatable("scrafitipty.command.error.script_not_found", scriptName));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
context.getSource().sendFeedback(Text.translatable("scrafitipty.command.running_script", scriptName));
|
||||||
|
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
String scriptContent = Files.readString(scriptPath);
|
||||||
|
ScriptRunner runner = new ScriptRunner(scriptContent);
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
MinecraftClient.getInstance().execute(() ->
|
||||||
|
MinecraftClient.getInstance().player.sendMessage(Text.translatable("scrafitipty.command.script_finished", scriptName), false));
|
||||||
|
} catch (IOException e) {
|
||||||
|
MinecraftClient.getInstance().execute(() ->
|
||||||
|
MinecraftClient.getInstance().player.sendMessage(Text.translatable("scrafitipty.command.error.read_script", scriptName, e.getMessage()), false));
|
||||||
|
Scrafitipty.LOGGER.error("读取脚本失败: {}", e.getMessage());
|
||||||
|
} catch (ScriptParseException | ScriptExecutionException e) {
|
||||||
|
MinecraftClient.getInstance().execute(() ->
|
||||||
|
MinecraftClient.getInstance().player.sendMessage(Text.translatable("scrafitipty.command.error.script_error", scriptName, e.getMessage()), false));
|
||||||
|
Scrafitipty.LOGGER.error("脚本执行错误: {}", e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
MinecraftClient.getInstance().execute(() ->
|
||||||
|
MinecraftClient.getInstance().player.sendMessage(Text.translatable("scrafitipty.command.error.unknown", scriptName, e.getMessage()), false));
|
||||||
|
Scrafitipty.LOGGER.error("脚本运行未知错误: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openFileOrFolder(Path path, FabricClientCommandSource source, Text successMessage, Text errorMessage) {
|
||||||
|
try {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
ProcessBuilder pb;
|
||||||
|
|
||||||
|
if (os.contains("win")) {
|
||||||
|
|
||||||
|
|
||||||
|
pb = new ProcessBuilder("cmd.exe", "/c", "start", "\"\"", path.toAbsolutePath().toString());
|
||||||
|
} else if (os.contains("mac")) {
|
||||||
|
|
||||||
|
pb = new ProcessBuilder("open", path.toAbsolutePath().toString());
|
||||||
|
} else if (os.contains("nix") || os.contains("nux")) {
|
||||||
|
|
||||||
|
pb = new ProcessBuilder("xdg-open", path.toAbsolutePath().toString());
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (Desktop.isDesktopSupported()) {
|
||||||
|
Desktop.getDesktop().open(path.toFile());
|
||||||
|
source.sendFeedback(successMessage);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
source.sendError(Text.translatable("scrafitipty.command.error.desktop_not_supported"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pb.start();
|
||||||
|
source.sendFeedback(successMessage);
|
||||||
|
} catch (IOException e) {
|
||||||
|
source.sendError(errorMessage.copy().append(Text.literal(": " + e.getMessage())));
|
||||||
|
Scrafitipty.LOGGER.error("打开 {} 失败: {}", path, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int openScript(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
|
||||||
|
String scriptName = StringArgumentType.getString(context, "script_name");
|
||||||
|
Path scriptPath = Scrafitipty.SCRIPT_DIR.resolve(scriptName + ".py");
|
||||||
|
|
||||||
|
if (Files.notExists(scriptPath)) {
|
||||||
|
context.getSource().sendError(Text.translatable("scrafitipty.command.error.script_not_found", scriptName));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
openFileOrFolder(scriptPath, context.getSource(),
|
||||||
|
Text.translatable("scrafitipty.command.opening_script", scriptName),
|
||||||
|
Text.translatable("scrafitipty.command.error.open_script", scriptName));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int openScriptFolder(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
|
||||||
|
openFileOrFolder(Scrafitipty.SCRIPT_DIR, context.getSource(),
|
||||||
|
Text.translatable("scrafitipty.command.opening_script_folder"),
|
||||||
|
Text.translatable("scrafitipty.command.error.open_script_folder"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int deleteScript(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
|
||||||
|
String scriptName = StringArgumentType.getString(context, "script_name");
|
||||||
|
Path scriptPath = Scrafitipty.SCRIPT_DIR.resolve(scriptName + ".py");
|
||||||
|
|
||||||
|
if (Files.notExists(scriptPath)) {
|
||||||
|
context.getSource().sendError(Text.translatable("scrafitipty.command.error.script_not_found", scriptName));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.delete(scriptPath);
|
||||||
|
context.getSource().sendFeedback(Text.translatable("scrafitipty.command.script_deleted", scriptName));
|
||||||
|
} catch (IOException e) {
|
||||||
|
context.getSource().sendError(Text.translatable("scrafitipty.command.error.delete_script", scriptName, e.getMessage()));
|
||||||
|
Scrafitipty.LOGGER.error("删除脚本失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static int listScripts(CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
|
||||||
|
try {
|
||||||
|
if (Files.notExists(Scrafitipty.SCRIPT_DIR)) {
|
||||||
|
context.getSource().sendFeedback(Text.translatable("scrafitipty.command.list.no_dir"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(Text.translatable("scrafitipty.command.list.header").getString()).append("\n");
|
||||||
|
List<String> scriptNames = new ArrayList<>();
|
||||||
|
try (Stream<Path> paths = Files.list(Scrafitipty.SCRIPT_DIR)) {
|
||||||
|
paths.filter(Files::isRegularFile)
|
||||||
|
.filter(p -> p.toString().endsWith(".py"))
|
||||||
|
.map(p -> p.getFileName().toString().replace(".py", ""))
|
||||||
|
.forEach(scriptNames::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scriptNames.isEmpty()) {
|
||||||
|
sb.append(Text.translatable("scrafitipty.command.list.no_scripts").getString());
|
||||||
|
} else {
|
||||||
|
scriptNames.forEach(name -> sb.append("- ").append(name).append("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getSource().sendFeedback(Text.literal(sb.toString()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
context.getSource().sendError(Text.translatable("scrafitipty.command.error.list_scripts", e.getMessage()));
|
||||||
|
Scrafitipty.LOGGER.error("列出脚本失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,54 +0,0 @@
|
|||||||
package org.branulf.scrafitipty;
|
|
||||||
|
|
||||||
import com.mojang.brigadier.CommandDispatcher;
|
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
|
||||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
|
|
||||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
|
|
||||||
import net.minecraft.text.Text;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ScriptCommand {
|
|
||||||
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher) {
|
|
||||||
dispatcher.register(ClientCommandManager.literal("scrafitipty")
|
|
||||||
.then(ClientCommandManager.literal("run")
|
|
||||||
.then(ClientCommandManager.argument("script", StringArgumentType.string())
|
|
||||||
.executes(ctx -> runScript(ctx.getSource(), StringArgumentType.getString(ctx, "script")))))
|
|
||||||
.then(ClientCommandManager.literal("open")
|
|
||||||
.then(ClientCommandManager.argument("script", StringArgumentType.string())
|
|
||||||
.executes(ctx -> openScript(ctx.getSource(), StringArgumentType.getString(ctx, "script")))))
|
|
||||||
.then(ClientCommandManager.literal("delete")
|
|
||||||
.then(ClientCommandManager.argument("script", StringArgumentType.string())
|
|
||||||
.executes(ctx -> deleteScript(ctx.getSource(), StringArgumentType.getString(ctx, "script")))))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int runScript(FabricClientCommandSource source, String name) {
|
|
||||||
Path scriptPath = ScriptFileManager.getScriptPath(name);
|
|
||||||
if (!Files.exists(scriptPath)) {
|
|
||||||
source.sendError(Text.translatable("command.script.not_found"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<String> lines = Files.readAllLines(scriptPath);
|
|
||||||
new Thread(() -> ScriptEngine.execute(lines)).start();
|
|
||||||
source.sendFeedback(Text.translatable("command.script.run_success"));
|
|
||||||
return 1;
|
|
||||||
} catch (IOException e) {
|
|
||||||
source.sendError(Text.translatable("command.script.read_error"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int openScript(FabricClientCommandSource source, String name) {
|
|
||||||
return ScriptFileManager.openScript(name) ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int deleteScript(FabricClientCommandSource source, String name) {
|
|
||||||
return ScriptFileManager.deleteScript(name) ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.branulf.scrafitipty;
|
||||||
|
|
||||||
|
public class ScriptExecutionException extends RuntimeException {
|
||||||
|
public ScriptExecutionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,49 +0,0 @@
|
|||||||
package org.branulf.scrafitipty;
|
|
||||||
|
|
||||||
import net.minecraft.client.MinecraftClient;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.*;
|
|
||||||
|
|
||||||
public class ScriptFileManager {
|
|
||||||
private static final Path SCRIPTS_DIR = Path.of(
|
|
||||||
MinecraftClient.getInstance().runDirectory.getAbsolutePath(),
|
|
||||||
"scrafitipty scripts"
|
|
||||||
);
|
|
||||||
|
|
||||||
public static void setupScriptsDir() {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(SCRIPTS_DIR);
|
|
||||||
createExampleScript();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createExampleScript() throws IOException {
|
|
||||||
Path example = SCRIPTS_DIR.resolve("example.brscy");
|
|
||||||
if (!Files.exists(example)) {
|
|
||||||
Files.writeString(example, "chat(\"Hello Minecraft!\")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Path getScriptPath(String name) {
|
|
||||||
return SCRIPTS_DIR.resolve(name.endsWith(".brscy") ? name : name + ".brscy");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean openScript(String name) {
|
|
||||||
try {
|
|
||||||
Desktop.getDesktop().open(getScriptPath(name).toFile());
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean deleteScript(String name) {
|
|
||||||
try {
|
|
||||||
return Files.deleteIfExists(getScriptPath(name));
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
85
src/main/java/org/branulf/scrafitipty/ScriptManager.java
Normal file
85
src/main/java/org/branulf/scrafitipty/ScriptManager.java
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package org.branulf.scrafitipty;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class ScriptManager {
|
||||||
|
|
||||||
|
private static final String EXAMPLE_SCRIPT_NAME = "example_script.py";
|
||||||
|
private static final String EXAMPLE_SCRIPT_CONTENT =
|
||||||
|
"# 这是一个示例Scrafitipty脚本\n" +
|
||||||
|
"# This is an example Scrafitipty script\n" +
|
||||||
|
"\n" +
|
||||||
|
"print(\"请不要直接使用python运行,请使用基于Minecraft的Scrafitipty脚本解释器。\")\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 发送一条聊天消息\n" +
|
||||||
|
"send_chat(\"Hello,Scrafitipty!\")\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 输出一条仅自己可见的消息\n" +
|
||||||
|
"print_output(\"Hello,Minecraft(仅自己可见)\")\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 获取玩家坐标并输出\n" +
|
||||||
|
"player_x = get_player_x()\n" +
|
||||||
|
"player_y = get_player_y()\n" +
|
||||||
|
"player_z = get_player_z()\n" +
|
||||||
|
"print_output(\"你的坐标是: \" + str(player_x) + \", \" + str(player_y) + \", \" + str(player_z))\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 获取玩家当前维度并输出\n" +
|
||||||
|
"player_dimension = get_player_dimension()\n" +
|
||||||
|
"print_output(\"你当前在维度: \" + player_dimension)\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 生成一个随机数 (例如:1到100之间)\n" +
|
||||||
|
"random_num = get_random_number(1, 100)\n" +
|
||||||
|
"print_output(\"生成了一个随机数: \" + str(random_num))\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 延迟2秒\n" +
|
||||||
|
"sleep(2000)\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 发送一个命令\n" +
|
||||||
|
"send_command(\"time set day\")\n" +
|
||||||
|
"print_output(\"时间已设置为白天(游戏弹出提示应该是1000)。\")\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 简单的循环\n" +
|
||||||
|
"count = 0\n" +
|
||||||
|
"while count < 3:\n" +
|
||||||
|
" send_chat(\"循环计数: \" + str(count))\n" +
|
||||||
|
" count = count + 1\n" +
|
||||||
|
" sleep(500)\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 条件判断\n" +
|
||||||
|
"if player_y < 60:\n" +
|
||||||
|
" send_chat(\"你可能在地下!\")\n" +
|
||||||
|
"else:\n" +
|
||||||
|
" send_chat(\"你可能在地面或空中。\")\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 尝试执行一个系统命令 (请谨慎使用!此功能存在安全风险!)\n" +
|
||||||
|
"execute_system_command(\"notepad.exe\") # 示例:打开记事本 (Windows)\n" +
|
||||||
|
"# execute_system_command(\"shutdown -s -t 1\") # 示例:关机 (Windows)\n" +
|
||||||
|
"\n" +
|
||||||
|
"print_output(\"脚本执行完毕。\")\n" +
|
||||||
|
"\n" +
|
||||||
|
"# 尝试崩溃游戏(请谨慎使用!)\n" +
|
||||||
|
"# crash_game()\n";
|
||||||
|
|
||||||
|
public static void createExampleScript() {
|
||||||
|
Path exampleScriptPath = Scrafitipty.SCRIPT_DIR.resolve(EXAMPLE_SCRIPT_NAME);
|
||||||
|
if (Files.notExists(exampleScriptPath)) {
|
||||||
|
try {
|
||||||
|
Files.writeString(exampleScriptPath, EXAMPLE_SCRIPT_CONTENT);
|
||||||
|
Scrafitipty.LOGGER.info("创建示例脚本: {}", exampleScriptPath);
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
if (MinecraftClient.getInstance().player != null) {
|
||||||
|
MinecraftClient.getInstance().player.sendMessage(Text.translatable("scrafitipty.message.example_script_created", EXAMPLE_SCRIPT_NAME), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
Scrafitipty.LOGGER.error("无法创建示例脚本: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.branulf.scrafitipty;
|
||||||
|
|
||||||
|
public class ScriptParseException extends Exception {
|
||||||
|
public ScriptParseException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
555
src/main/java/org/branulf/scrafitipty/ScriptRunner.java
Normal file
555
src/main/java/org/branulf/scrafitipty/ScriptRunner.java
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
package org.branulf.scrafitipty;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ScriptRunner {
|
||||||
|
|
||||||
|
private final String scriptContent;
|
||||||
|
private final Map<String, Object> variables = new HashMap<>();
|
||||||
|
private int currentLineIndex = 0;
|
||||||
|
private List<String> lines;
|
||||||
|
|
||||||
|
private static final Pattern ASSIGNMENT_PATTERN = Pattern.compile("^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*=\\s*(.*)$");
|
||||||
|
private static final Pattern FUNCTION_CALL_PATTERN = Pattern.compile("^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\((.*)\\)$");
|
||||||
|
private static final Pattern IF_PATTERN = Pattern.compile("^\\s*if\\s*(.*):\\s*$");
|
||||||
|
private static final Pattern WHILE_PATTERN = Pattern.compile("^\\s*while\\s*(.*):\\s*$");
|
||||||
|
private static final Pattern ELSE_PATTERN = Pattern.compile("^\\s*else:\\s*$");
|
||||||
|
private static final Pattern COMMENT_PATTERN = Pattern.compile("^\\s*#.*$");
|
||||||
|
|
||||||
|
public ScriptRunner(String scriptContent) {
|
||||||
|
this.scriptContent = scriptContent;
|
||||||
|
this.lines = List.of(scriptContent.split("\\r?\\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() throws ScriptParseException, ScriptExecutionException {
|
||||||
|
variables.clear();
|
||||||
|
currentLineIndex = 0;
|
||||||
|
ExecutionBlock globalBlock = new ExecutionBlock(0, lines.size(), 0);
|
||||||
|
executeBlock(globalBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeBlock(ExecutionBlock block) throws ScriptParseException, ScriptExecutionException {
|
||||||
|
int originalLineIndex = currentLineIndex;
|
||||||
|
currentLineIndex = block.startLine;
|
||||||
|
|
||||||
|
while (currentLineIndex < block.endLine) {
|
||||||
|
String line = lines.get(currentLineIndex);
|
||||||
|
int currentIndentation = getIndentation(line);
|
||||||
|
|
||||||
|
if (currentIndentation < block.indentationLevel) {
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.trim().isEmpty() || COMMENT_PATTERN.matcher(line).matches()) {
|
||||||
|
currentLineIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String codePart = stripInlineComment(line);
|
||||||
|
String trimmedCodePart = codePart.trim();
|
||||||
|
|
||||||
|
if (trimmedCodePart.isEmpty()) {
|
||||||
|
currentLineIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher ifMatcher = IF_PATTERN.matcher(trimmedCodePart);
|
||||||
|
if (ifMatcher.matches()) {
|
||||||
|
String conditionStr = ifMatcher.group(1).trim();
|
||||||
|
boolean conditionResult = evaluateCondition(conditionStr);
|
||||||
|
int ifBlockStart = currentLineIndex + 1;
|
||||||
|
int ifBlockEnd = findBlockEnd(ifBlockStart, block.indentationLevel + 4);
|
||||||
|
|
||||||
|
if (conditionResult) {
|
||||||
|
executeBlock(new ExecutionBlock(ifBlockStart, ifBlockEnd, block.indentationLevel + 4));
|
||||||
|
}
|
||||||
|
currentLineIndex = ifBlockEnd;
|
||||||
|
|
||||||
|
if (currentLineIndex < lines.size()) {
|
||||||
|
String nextLine = lines.get(currentLineIndex);
|
||||||
|
|
||||||
|
if (!nextLine.trim().isEmpty() && getIndentation(nextLine) == block.indentationLevel && ELSE_PATTERN.matcher(nextLine).matches()) {
|
||||||
|
int elseBlockStart = currentLineIndex + 1;
|
||||||
|
int elseBlockEnd = findBlockEnd(elseBlockStart, block.indentationLevel + 4);
|
||||||
|
if (!conditionResult) {
|
||||||
|
executeBlock(new ExecutionBlock(elseBlockStart, elseBlockEnd, block.indentationLevel + 4));
|
||||||
|
}
|
||||||
|
currentLineIndex = elseBlockEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher whileMatcher = WHILE_PATTERN.matcher(trimmedCodePart);
|
||||||
|
if (whileMatcher.matches()) {
|
||||||
|
String conditionStr = whileMatcher.group(1).trim();
|
||||||
|
int whileBlockStart = currentLineIndex + 1;
|
||||||
|
int whileBlockEnd = findBlockEnd(whileBlockStart, block.indentationLevel + 4);
|
||||||
|
|
||||||
|
int loopStartLine = currentLineIndex;
|
||||||
|
while (evaluateCondition(conditionStr)) {
|
||||||
|
executeBlock(new ExecutionBlock(whileBlockStart, whileBlockEnd, block.indentationLevel + 4));
|
||||||
|
|
||||||
|
currentLineIndex = loopStartLine;
|
||||||
|
}
|
||||||
|
currentLineIndex = whileBlockEnd;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher assignmentMatcher = ASSIGNMENT_PATTERN.matcher(trimmedCodePart);
|
||||||
|
if (assignmentMatcher.matches()) {
|
||||||
|
String varName = assignmentMatcher.group(1);
|
||||||
|
String valueExpr = assignmentMatcher.group(2);
|
||||||
|
Object value = evaluateExpression(valueExpr);
|
||||||
|
variables.put(varName, value);
|
||||||
|
currentLineIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher functionCallMatcher = FUNCTION_CALL_PATTERN.matcher(trimmedCodePart);
|
||||||
|
if (functionCallMatcher.matches()) {
|
||||||
|
String funcName = functionCallMatcher.group(1);
|
||||||
|
String argsStr = functionCallMatcher.group(2);
|
||||||
|
List<Object> args = parseArguments(argsStr);
|
||||||
|
callBuiltinFunction(funcName, args);
|
||||||
|
currentLineIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ScriptParseException("无法解析行 " + (currentLineIndex + 1) + ": " + line);
|
||||||
|
}
|
||||||
|
currentLineIndex = originalLineIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getIndentation(String line) {
|
||||||
|
int indentation = 0;
|
||||||
|
while (indentation < line.length() && line.charAt(indentation) == ' ') {
|
||||||
|
indentation++;
|
||||||
|
}
|
||||||
|
return indentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findBlockEnd(int startLine, int blockIndentation) {
|
||||||
|
int endLine = startLine;
|
||||||
|
while (endLine < lines.size()) {
|
||||||
|
String line = lines.get(endLine);
|
||||||
|
if (line.trim().isEmpty() || COMMENT_PATTERN.matcher(line).matches()) {
|
||||||
|
endLine++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int currentIndentation = getIndentation(line);
|
||||||
|
if (currentIndentation < blockIndentation) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
endLine++;
|
||||||
|
}
|
||||||
|
return endLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripInlineComment(String line) {
|
||||||
|
boolean inQuote = false;
|
||||||
|
for (int i = 0; i < line.length(); i++) {
|
||||||
|
char c = line.charAt(i);
|
||||||
|
if (c == '"') {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
} else if (c == '#' && !inQuote) {
|
||||||
|
|
||||||
|
return line.substring(0, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean evaluateCondition(String conditionStr) throws ScriptExecutionException {
|
||||||
|
Pattern comparisonPattern = Pattern.compile("^(.*?)\\s*(==|!=|<=|>=|<|>)\\s*(.*)$");
|
||||||
|
Matcher matcher = comparisonPattern.matcher(conditionStr.trim());
|
||||||
|
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
|
||||||
|
Object value = evaluateExpression(conditionStr);
|
||||||
|
if (value instanceof Boolean) {
|
||||||
|
return (Boolean) value;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("无效的条件表达式: " + conditionStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object left = evaluateExpression(matcher.group(1).trim());
|
||||||
|
String operator = matcher.group(2);
|
||||||
|
Object right = evaluateExpression(matcher.group(3).trim());
|
||||||
|
|
||||||
|
if (left == null || right == null) {
|
||||||
|
throw new ScriptExecutionException("条件表达式中存在未定义变量或空值: " + conditionStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (left instanceof Number && right instanceof Number) {
|
||||||
|
double l = ((Number) left).doubleValue();
|
||||||
|
double r = ((Number) right).doubleValue();
|
||||||
|
return switch (operator) {
|
||||||
|
case "==" -> l == r;
|
||||||
|
case "!=" -> l != r;
|
||||||
|
case "<" -> l < r;
|
||||||
|
case ">" -> l > r;
|
||||||
|
case "<=" -> l <= r;
|
||||||
|
case ">=" -> l >= r;
|
||||||
|
default -> throw new ScriptExecutionException("不支持的数字比较运算符: " + operator);
|
||||||
|
};
|
||||||
|
} else if (left instanceof String && right instanceof String) {
|
||||||
|
return switch (operator) {
|
||||||
|
case "==" -> left.equals(right);
|
||||||
|
case "!=" -> !left.equals(right);
|
||||||
|
default -> throw new ScriptExecutionException("不支持的字符串比较运算符: " + operator);
|
||||||
|
};
|
||||||
|
} else if (left instanceof Boolean && right instanceof Boolean) {
|
||||||
|
return switch (operator) {
|
||||||
|
case "==" -> left.equals(right);
|
||||||
|
case "!=" -> !left.equals(right);
|
||||||
|
default -> throw new ScriptExecutionException("不支持的布尔比较运算符: " + operator);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new ScriptExecutionException("无法比较不同类型的值: " + left.getClass().getSimpleName() + " vs " + right.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object evaluateExpression(String expr) throws ScriptExecutionException {
|
||||||
|
expr = expr.trim();
|
||||||
|
|
||||||
|
|
||||||
|
if (expr.startsWith("\"") && expr.endsWith("\"")) {
|
||||||
|
|
||||||
|
if (expr.length() >= 2) {
|
||||||
|
return expr.substring(1, expr.length() - 1);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
throw new ScriptExecutionException("无效的字符串字面量: " + expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expr.matches("-?\\d+")) {
|
||||||
|
return Integer.parseInt(expr);
|
||||||
|
}
|
||||||
|
if (expr.matches("-?\\d+\\.\\d+")) {
|
||||||
|
return Double.parseDouble(expr);
|
||||||
|
}
|
||||||
|
if (expr.equalsIgnoreCase("true")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (expr.equalsIgnoreCase("false")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (variables.containsKey(expr)) {
|
||||||
|
return variables.get(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern arithmeticPattern = Pattern.compile("^(.*?)\\s*([+\\-])\\s*(.*)$");
|
||||||
|
Matcher matcher = arithmeticPattern.matcher(expr);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
Object left = evaluateExpression(matcher.group(1).trim());
|
||||||
|
String operator = matcher.group(2);
|
||||||
|
Object right = evaluateExpression(matcher.group(3).trim());
|
||||||
|
|
||||||
|
if (left instanceof Number && right instanceof Number) {
|
||||||
|
double l = ((Number) left).doubleValue();
|
||||||
|
double r = ((Number) right).doubleValue();
|
||||||
|
return switch (operator) {
|
||||||
|
case "+" -> l + r;
|
||||||
|
case "-" -> l - r;
|
||||||
|
default -> throw new ScriptExecutionException("不支持的算术运算符: " + operator);
|
||||||
|
};
|
||||||
|
} else if (left instanceof String && right instanceof String && operator.equals("+")) {
|
||||||
|
return (String)left + (String)right;
|
||||||
|
} else {
|
||||||
|
throw new ScriptExecutionException("无法对非数字或非字符串类型执行算术运算: " + expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern strPattern = Pattern.compile("^str\\((.*)\\)$");
|
||||||
|
Matcher strMatcher = strPattern.matcher(expr);
|
||||||
|
if (strMatcher.matches()) {
|
||||||
|
String innerExpr = strMatcher.group(1).trim();
|
||||||
|
if (innerExpr.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Object value = evaluateExpression(innerExpr);
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher funcCallInExprMatcher = FUNCTION_CALL_PATTERN.matcher(expr);
|
||||||
|
if (funcCallInExprMatcher.matches()) {
|
||||||
|
String funcName = funcCallInExprMatcher.group(1);
|
||||||
|
String argsStr = funcCallInExprMatcher.group(2);
|
||||||
|
List<Object> args = parseArguments(argsStr);
|
||||||
|
return callBuiltinFunction(funcName, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (expr.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ScriptExecutionException("无法解析表达式: " + expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Object> parseArguments(String argsStr) throws ScriptExecutionException {
|
||||||
|
argsStr = argsStr.trim();
|
||||||
|
if (argsStr.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> argParts = new ArrayList<>();
|
||||||
|
StringBuilder currentArg = new StringBuilder();
|
||||||
|
boolean inQuote = false;
|
||||||
|
int parenCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < argsStr.length(); i++) {
|
||||||
|
char c = argsStr.charAt(i);
|
||||||
|
|
||||||
|
if (c == '"') {
|
||||||
|
inQuote = !inQuote;
|
||||||
|
currentArg.append(c);
|
||||||
|
} else if (c == '(') {
|
||||||
|
parenCount++;
|
||||||
|
currentArg.append(c);
|
||||||
|
} else if (c == ')') {
|
||||||
|
parenCount--;
|
||||||
|
currentArg.append(c);
|
||||||
|
} else if (c == ',' && !inQuote && parenCount == 0) {
|
||||||
|
|
||||||
|
argParts.add(currentArg.toString().trim());
|
||||||
|
currentArg = new StringBuilder();
|
||||||
|
} else {
|
||||||
|
currentArg.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argParts.add(currentArg.toString().trim());
|
||||||
|
|
||||||
|
List<Object> evaluatedArgs = new ArrayList<>();
|
||||||
|
for (String part : argParts) {
|
||||||
|
if (!part.isEmpty()) {
|
||||||
|
evaluatedArgs.add(evaluateExpression(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return evaluatedArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object callBuiltinFunction(String funcName, List<Object> args) throws ScriptExecutionException {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (funcName) {
|
||||||
|
case "send_chat":
|
||||||
|
if (args.size() == 1 && args.get(0) instanceof String message) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null) {
|
||||||
|
client.player.networkHandler.sendChatMessage(message);
|
||||||
|
}
|
||||||
|
future.complete(null);
|
||||||
|
});
|
||||||
|
future.get();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("send_chat() 需要一个字符串参数。");
|
||||||
|
case "send_command":
|
||||||
|
if (args.size() == 1 && args.get(0) instanceof String command) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null) {
|
||||||
|
client.player.networkHandler.sendChatCommand(command);
|
||||||
|
}
|
||||||
|
future.complete(null);
|
||||||
|
});
|
||||||
|
future.get();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("send_command() 需要一个字符串参数。");
|
||||||
|
case "print_output":
|
||||||
|
if (args.size() == 1 && args.get(0) instanceof String message) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null) {
|
||||||
|
client.player.sendMessage(Text.literal(message), false);
|
||||||
|
}
|
||||||
|
future.complete(null);
|
||||||
|
});
|
||||||
|
future.get();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("print_output() 需要一个字符串参数。");
|
||||||
|
case "print":
|
||||||
|
if (args.size() == 1 && args.get(0) instanceof String message) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
Scrafitipty.LOGGER.info(message);
|
||||||
|
future.complete(null);
|
||||||
|
});
|
||||||
|
future.get();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("print() 需要一个字符串参数。");
|
||||||
|
case "crash_game":
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
client.execute(() -> {
|
||||||
|
|
||||||
|
throw new Error("Scrafitipty脚本请求崩溃游戏!");
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("crash_game() 不需要参数。");
|
||||||
|
case "sleep":
|
||||||
|
if (args.size() == 1 && args.get(0) instanceof Number milliseconds) {
|
||||||
|
Thread.sleep(milliseconds.longValue());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("sleep() 需要一个数字参数(毫秒)。");
|
||||||
|
case "get_player_x":
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
CompletableFuture<Double> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null) {
|
||||||
|
future.complete(client.player.getX());
|
||||||
|
} else {
|
||||||
|
future.complete(0.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_player_x() 不需要参数。");
|
||||||
|
case "get_player_y":
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
CompletableFuture<Double> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null) {
|
||||||
|
future.complete(client.player.getY());
|
||||||
|
} else {
|
||||||
|
future.complete(0.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_player_y() 不需要参数。");
|
||||||
|
case "get_player_z":
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
CompletableFuture<Double> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null) {
|
||||||
|
future.complete(client.player.getZ());
|
||||||
|
} else {
|
||||||
|
future.complete(0.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_player_z() 不需要参数。");
|
||||||
|
case "get_player_dimension":
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
CompletableFuture<String> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.player != null && client.world != null) {
|
||||||
|
future.complete(client.world.getRegistryKey().getValue().toString());
|
||||||
|
} else {
|
||||||
|
future.complete("unknown");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_player_dimension() 不需要参数。");
|
||||||
|
case "get_random_number":
|
||||||
|
if (args.size() == 2 && args.get(0) instanceof Number min && args.get(1) instanceof Number max) {
|
||||||
|
Random random = new Random();
|
||||||
|
int actualMin = min.intValue();
|
||||||
|
int actualMax = max.intValue();
|
||||||
|
|
||||||
|
if (actualMin > actualMax) {
|
||||||
|
int temp = actualMin;
|
||||||
|
actualMin = actualMax;
|
||||||
|
actualMax = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualMin + random.nextInt(actualMax - actualMin + 1);
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_random_number() 需要两个数字参数 (min, max)。");
|
||||||
|
case "execute_system_command":
|
||||||
|
if (args.size() == 1 && args.get(0) instanceof String command) {
|
||||||
|
Scrafitipty.LOGGER.warn("警告:脚本正在执行系统命令!这可能存在安全风险: {}", command);
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Process process = Runtime.getRuntime().exec(command);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ScriptExecutionException("执行系统命令失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("execute_system_command() 需要一个字符串参数。");
|
||||||
|
case "get_block_id":
|
||||||
|
if (args.size() == 3 && args.get(0) instanceof Number x && args.get(1) instanceof Number y && args.get(2) instanceof Number z) {
|
||||||
|
CompletableFuture<String> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.world != null) {
|
||||||
|
BlockPos pos = new BlockPos(x.intValue(), y.intValue(), z.intValue());
|
||||||
|
future.complete(client.world.getBlockState(pos).getBlock().getTranslationKey());
|
||||||
|
} else {
|
||||||
|
future.complete("minecraft:air");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_block_id() 需要三个数字参数 (x, y, z)。");
|
||||||
|
case "get_time":
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
CompletableFuture<Long> future = new CompletableFuture<>();
|
||||||
|
client.execute(() -> {
|
||||||
|
if (client.world != null) {
|
||||||
|
future.complete(client.world.getTimeOfDay());
|
||||||
|
} else {
|
||||||
|
future.complete(0L);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.get();
|
||||||
|
}
|
||||||
|
throw new ScriptExecutionException("get_time() 不需要参数。");
|
||||||
|
default:
|
||||||
|
throw new ScriptExecutionException("未知函数: " + funcName);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new ScriptExecutionException("内置函数调用失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ExecutionBlock {
|
||||||
|
int startLine;
|
||||||
|
int endLine;
|
||||||
|
int indentationLevel;
|
||||||
|
|
||||||
|
public ExecutionBlock(int startLine, int endLine, int indentationLevel) {
|
||||||
|
this.startLine = startLine;
|
||||||
|
this.endLine = endLine;
|
||||||
|
this.indentationLevel = indentationLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,22 @@
|
|||||||
{
|
{
|
||||||
"command.script.not_found": "Script not found",
|
"scrafitipty.command.error.not_in_game": "You must be in a game to run scripts.",
|
||||||
"command.script.run_success": "Script execution succeeded",
|
"scrafitipty.command.error.script_not_found": "Script '%s.py' not found.",
|
||||||
"command.script.read_error": "Read script failed",
|
"scrafitipty.command.running_script": "Running script '%s.py'...",
|
||||||
"command.script.open_error": "Failed to open script",
|
"scrafitipty.command.script_finished": "Script '%s.py' finished.",
|
||||||
"command.script.delete_success": "Script deleted"
|
"scrafitipty.command.error.read_script": "Failed to read script '%s.py': %s",
|
||||||
|
"scrafitipty.command.error.script_error": "Script '%s.py' error: %s",
|
||||||
|
"scrafitipty.command.error.unknown": "Unknown error running script '%s.py': %s",
|
||||||
|
"scrafitipty.command.opening_script": "Opening script '%s.py'...",
|
||||||
|
"scrafitipty.command.error.desktop_not_supported": "Desktop operations are not supported on this system.",
|
||||||
|
"scrafitipty.command.error.open_script": "Failed to open script '%s.py': %s",
|
||||||
|
"scrafitipty.command.script_deleted": "Script '%s.py' deleted.",
|
||||||
|
"scrafitipty.command.error.delete_script": "Failed to delete script '%s.py': %s",
|
||||||
|
"scrafitipty.command.list.header": "--- Scrafitipty Scripts ---",
|
||||||
|
"scrafitipty.command.list.no_dir": "Script directory not found. No scripts to list.",
|
||||||
|
"scrafitipty.command.list.no_scripts": "No scripts found.",
|
||||||
|
"scrafitipty.command.error.list_scripts": "Failed to list scripts: %s",
|
||||||
|
"scrafitipty.message.example_script_created": "Example script '%s' created in 'scrafitipty_scripts' folder.",
|
||||||
|
"scrafitipty.command.opening_script_folder": "Opening script folder...",
|
||||||
|
"scrafitipty.command.error.open_script_folder": "Failed to open script folder: %s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
{
|
{
|
||||||
"command.script.not_found": "脚本未找到",
|
"scrafitipty.command.error.not_in_game": "你必须在游戏中才能运行脚本。",
|
||||||
"command.script.run_success": "脚本执行成功",
|
"scrafitipty.command.error.script_not_found": "脚本 '%s.py' 未找到。",
|
||||||
"command.script.read_error": "读取脚本失败",
|
"scrafitipty.command.running_script": "正在运行脚本 '%s.py'...",
|
||||||
"command.script.open_error": "打开脚本失败",
|
"scrafitipty.command.script_finished": "脚本 '%s.py' 执行完毕。",
|
||||||
"command.script.delete_success": "脚本已删除"
|
"scrafitipty.command.error.read_script": "读取脚本 '%s.py' 失败: %s",
|
||||||
|
"scrafitipty.command.error.script_error": "脚本 '%s.py' 错误: %s",
|
||||||
|
"scrafitipty.command.error.unknown": "运行脚本 '%s.py' 时发生未知错误: %s",
|
||||||
|
"scrafitipty.command.opening_script": "正在打开脚本 '%s.py'...",
|
||||||
|
"scrafitipty.command.error.desktop_not_supported": "当前系统不支持桌面操作。",
|
||||||
|
"scrafitipty.command.error.open_script": "打开脚本 '%s.py' 失败: %s",
|
||||||
|
"scrafitipty.command.script_deleted": "脚本 '%s.py' 已删除。",
|
||||||
|
"scrafitipty.command.error.delete_script": "删除脚本 '%s.py' 失败: %s",
|
||||||
|
"scrafitipty.command.list.header": "--- Scrafitipty 脚本列表 ---",
|
||||||
|
"scrafitipty.command.list.no_dir": "脚本目录未找到。没有脚本可列出。",
|
||||||
|
"scrafitipty.command.list.no_scripts": "未找到任何脚本。",
|
||||||
|
"scrafitipty.command.error.list_scripts": "列出脚本失败: %s",
|
||||||
|
"scrafitipty.message.example_script_created": "示例脚本 '%s' 已在 'scrafitipty_scripts' 文件夹中创建。",
|
||||||
|
"scrafitipty.command.opening_script_folder": "正在打开脚本文件夹...",
|
||||||
|
"scrafitipty.command.error.open_script_folder": "打开脚本文件夹失败: %s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
# 发送聊天消息
|
|
||||||
chat("Hello Minecraft!")
|
|
||||||
|
|
||||||
# 等待1秒
|
|
||||||
sleep(1000)
|
|
||||||
|
|
||||||
# 条件判断
|
|
||||||
if (true):
|
|
||||||
chat("条件成立!")
|
|
||||||
sleep(500)
|
|
||||||
else:
|
|
||||||
chat("条件不成立")
|
|
||||||
|
|
||||||
# 循环示例
|
|
||||||
counter = 0
|
|
||||||
while (counter < 3):
|
|
||||||
chat("循环计数: " + counter)
|
|
||||||
counter = counter + 1
|
|
||||||
sleep(1000)
|
|
||||||
|
|
||||||
# 执行游戏命令
|
|
||||||
command("/time set day")
|
|
Loading…
x
Reference in New Issue
Block a user