半成品

This commit is contained in:
BRanulf_Explode 2025-07-29 14:04:21 +08:00
parent badaf12bf5
commit 62204064e9
15 changed files with 1068 additions and 179 deletions

106
README.md Normal file
View File

@ -0,0 +1,106 @@
# Scrafitipty暂定名称 - 脚本执行器
![Minecraft Fabric](https://img.shields.io/badge/Minecraft-1.21.4-green) ![Mod Loader](https://img.shields.io/badge/Mod%20Loader-Fabric-blue)
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"
# 字符串转数字(需要手动实现)
# 当前版本暂不支持自动转换
```

View File

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

View File

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

View File

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

View File

@ -1,14 +1,39 @@
package org.branulf.scrafitipty; 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 初始化完成。");
} }
} }

View 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;
}
}

View File

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

View File

@ -0,0 +1,7 @@
package org.branulf.scrafitipty;
public class ScriptExecutionException extends RuntimeException {
public ScriptExecutionException(String message) {
super(message);
}
}

View File

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

View 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(\"HelloScrafitipty\")\n" +
"\n" +
"# 输出一条仅自己可见的消息\n" +
"print_output(\"HelloMinecraft(仅自己可见)\")\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());
}
}
}
}

View File

@ -0,0 +1,7 @@
package org.branulf.scrafitipty;
public class ScriptParseException extends Exception {
public ScriptParseException(String message) {
super(message);
}
}

View 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;
}
}
}

View File

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

View File

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

View File

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