4 Commits
Author SHA1 Message Date
database-mysql 3c5621b9b9 fix: persist flight state and fly speed per player (JSON config), restore on join/respawn
build / build (push) Has been cancelled
2026-10-19 13:26:41 +08:00
database-mysql b91166b5d6 editor fly min/max speed
build / build (push) Has been cancelled
2026-10-19 13:20:12 +08:00
database-mysql 7f308bfa00 update gradle.properties version
build / build (push) Has been cancelled
2026-10-19 13:06:35 +08:00
database-mysql bfbdfd2199 feat: add /fly speed <0.1-2.0> command, rename admin command to /fly player <player>, bump version to 1.1.0 2026-10-19 12:56:12 +08:00
5 changed files with 204 additions and 41 deletions

No files matched your search

+30 -9
View File
@@ -5,8 +5,9 @@
## 功能特性 ## 功能特性
- 🕊️ **自由飞行** — 通过命令开启/关闭创造模式飞行能力 - 🕊️ **自由飞行** — 通过命令开启/关闭创造模式飞行能力
- ⚡ **飞行速度调节** — 通过命令在 0.01–0.15 范围内自定义飞行速度
- 🛡️ **摔落保护** — 飞行状态下免疫摔落伤害 - 🛡️ **摔落保护** — 飞行状态下免疫摔落伤害
- 💾 **状态持久化** — 重新加入服务器时自动恢复飞行状态 - 💾 **状态持久化** — 飞行状态与速度保存到配置文件,退出重进或服务器重启后自动恢复
- 👥 **多玩家支持** — 管理员可为其他玩家切换飞行状态 - 👥 **多玩家支持** — 管理员可为其他玩家切换飞行状态
## 环境要求 ## 环境要求
@@ -36,12 +37,24 @@
- 如果当前未飞行,执行后**开启飞行** - 如果当前未飞行,执行后**开启飞行**
- 如果当前正在飞行,执行后**关闭飞行** - 如果当前正在飞行,执行后**关闭飞行**
### `/fly <player>` ### `/fly speed <0.01-0.15>`
设置自己的飞行速度,范围为 0.01 到 0.15(默认 0.05)。所有玩家均可执行。
```
/fly speed 0.10
```
- 数值越大飞行越快,0.15 为允许的最大速度
- 设置后立即生效,无需重新开启飞行
- 速度会与飞行状态一起保存,退出重进后仍然保留
### `/fly player <player>`
切换指定玩家的飞行状态。需要 OP 权限(权限等级 2)。 切换指定玩家的飞行状态。需要 OP 权限(权限等级 2)。
``` ```
/fly Steve /fly player Steve
``` ```
- 如果目标玩家未飞行,执行后**为其开启飞行** - 如果目标玩家未飞行,执行后**为其开启飞行**
@@ -59,17 +72,25 @@
- **双击空格** — 切换飞行/普通状态 - **双击空格** — 切换飞行/普通状态
3. 再次输入 `/fly` 关闭飞行,回到普通模式 3. 再次输入 `/fly` 关闭飞行,回到普通模式
### 飞行速度
使用 `/fly speed <数值>` 调整飞行速度(0.01–0.15):
- `/fly speed 0.03` — 较慢的飞行速度
- `/fly speed 0.05` — 默认速度
- `/fly speed 0.15` — 最快速度
### 管理员操作 ### 管理员操作
作为服务器管理员,你可以为其他玩家管理飞行权限: 作为服务器管理员,你可以为其他玩家管理飞行权限:
``` ```
/fly <玩家名> /fly player <玩家名>
``` ```
例如: 例如:
- `/fly Steve` — 为 Steve 开启飞行 - `/fly player Steve` — 为 Steve 开启飞行
- `/fly Alex` — 关闭 Alex 的飞行 - `/fly player Alex` — 关闭 Alex 的飞行
### 摔落保护 ### 摔落保护
@@ -77,13 +98,13 @@
### 重连恢复 ### 重连恢复
如果玩家在飞行状态下断开连接,重新加入服务器时飞行状态会自动恢复。 玩家的飞行状态(是否飞行)与飞行速度会持久化保存。退出游戏后重新加入,甚至服务器重启,都会自动恢复上次的飞行状态与速度。
## FAQ ## FAQ
### Q: 飞行状态会一直保留吗? ### Q: 飞行状态会一直保留吗?
A: 飞行状态在服务器运行期间持续有效。如果你关闭了飞行、或者服务器重启,飞行状态会被重置。但如果你是断开重连,飞行状态会自动恢复。 A: 会。飞行状态(是否飞行)与飞行速度会保存到服务端的 `config/openfly_flight_data.json` 文件。玩家退出重进、服务器重启后都会自动恢复上次的状态。
### Q: 飞行时还会受到摔落伤害吗? ### Q: 飞行时还会受到摔落伤害吗?
@@ -91,7 +112,7 @@ A: 不会。飞行状态下免疫所有摔落伤害,关闭飞行后恢复正
### Q: 普通玩家可以使用 `/fly` 吗? ### Q: 普通玩家可以使用 `/fly` 吗?
A: 可以。`/fly` 命令对所有玩家开放,无需 OP 权限。但 `/fly <player>` 需要 OP 权限。 A: 可以。`/fly` 与 `/fly speed` 命令对所有玩家开放,无需 OP 权限。但 `/fly player <player>` 需要 OP 权限。
### Q: 这个模组需要安装在客户端还是服务端? ### Q: 这个模组需要安装在客户端还是服务端?
+1 -1
View File
@@ -1,5 +1,5 @@
plugins { plugins {
id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" id 'fabric-loom' version "${loom_version}"
id 'maven-publish' id 'maven-publish'
} }
+1 -1
View File
@@ -11,7 +11,7 @@ loader_version=0.19.5
loom_version=1.18-SNAPSHOT loom_version=1.18-SNAPSHOT
# Mod Properties # Mod Properties
version=1.0.0 version=1.2.0-1.21.8
group=top.gtb520.java.mods.openfly group=top.gtb520.java.mods.openfly
# Dependencies # Dependencies
@@ -1,23 +1,47 @@
package top.gtb520.java.mods.openfly.Commands; package top.gtb520.java.mods.openfly.Commands;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.mojang.brigadier.Command; import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.FloatArgumentType;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.command.argument.EntityArgumentType; import net.minecraft.command.argument.EntityArgumentType;
import net.minecraft.entity.damage.DamageTypes; import net.minecraft.entity.damage.DamageTypes;
import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.CommandManager;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import top.gtb520.java.mods.openfly.OpenFly;
import java.util.HashSet; import java.io.IOException;
import java.util.Set; import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
public class fly { public class fly {
// 记录开启了飞行的玩家UUID // 默认飞行速度(与原版一致)
private static final Set<UUID> flyingPlayers = new HashSet<>(); public static final float DEFAULT_FLY_SPEED = 0.05F;
// 飞行速度范围(/fly speed 参数范围)
public static final float MIN_FLY_SPEED = 0.01F;
public static final float MAX_FLY_SPEED = 0.15F;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
// 记录每个玩家的飞行状态(是否飞行 + 飞行速度),内存缓存 + JSON 持久化
private static final Map<UUID, PlayerFlightState> flightStates = new HashMap<>();
private static boolean loaded = false;
public static void register() { public static void register() {
loadStates();
// 注册命令 // 注册命令
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register( dispatcher.register(
@@ -31,7 +55,7 @@ public class fly {
} }
UUID playerId = player.getUuid(); UUID playerId = player.getUuid();
if (flyingPlayers.contains(playerId)) { if (isFlying(playerId)) {
disableFly(player); disableFly(player);
context.getSource().sendFeedback(() -> Text.literal("已禁用飞行"), false); context.getSource().sendFeedback(() -> Text.literal("已禁用飞行"), false);
} else { } else {
@@ -40,31 +64,52 @@ public class fly {
} }
return Command.SINGLE_SUCCESS; return Command.SINGLE_SUCCESS;
}) })
// /fly <player> - 切换指定玩家的飞行状态(需要OP权限) // /fly speed <0.01-0.15> - 设置自己的飞行速度(所有玩家可用)
.then(CommandManager.argument("player", EntityArgumentType.player()) .then(CommandManager.literal("speed")
.requires(source -> source.hasPermissionLevel(2)) .then(CommandManager.argument("speed", FloatArgumentType.floatArg(MIN_FLY_SPEED, MAX_FLY_SPEED))
.executes(context -> { .executes(context -> {
ServerPlayerEntity targetPlayer = EntityArgumentType.getPlayer(context, "player"); ServerPlayerEntity player = context.getSource().getPlayer();
UUID playerId = targetPlayer.getUuid(); if (player == null) {
context.getSource().sendFeedback(() -> Text.literal("该命令只能由玩家执行"), false);
return 0;
}
if (flyingPlayers.contains(playerId)) { float speed = FloatArgumentType.getFloat(context, "speed");
disableFly(targetPlayer); PlayerFlightState state = getOrCreateState(player.getUuid());
context.getSource().sendFeedback( state.flySpeed = speed;
() -> Text.literal("已禁用玩家 " + targetPlayer.getName().getString() + " 的飞行"), true); player.getAbilities().setFlySpeed(speed);
} else { player.sendAbilitiesUpdate();
enableFly(targetPlayer); saveStates();
context.getSource().sendFeedback( context.getSource().sendFeedback(
() -> Text.literal("已启用玩家 " + targetPlayer.getName().getString() + " 的飞行"), true); () -> Text.literal("飞行速度已设置为 " + String.format("%.2f", speed)), false);
} return Command.SINGLE_SUCCESS;
return Command.SINGLE_SUCCESS; })))
})) // /fly player <player> - 切换指定玩家的飞行状态(需要OP权限)
.then(CommandManager.literal("player")
.requires(source -> source.hasPermissionLevel(2))
.then(CommandManager.argument("player", EntityArgumentType.player())
.executes(context -> {
ServerPlayerEntity targetPlayer = EntityArgumentType.getPlayer(context, "player");
UUID playerId = targetPlayer.getUuid();
if (isFlying(playerId)) {
disableFly(targetPlayer);
context.getSource().sendFeedback(
() -> Text.literal("已禁用玩家 " + targetPlayer.getName().getString() + " 的飞行"), true);
} else {
enableFly(targetPlayer);
context.getSource().sendFeedback(
() -> Text.literal("已启用玩家 " + targetPlayer.getName().getString() + " 的飞行"), true);
}
return Command.SINGLE_SUCCESS;
})))
); );
}); });
// 注册摔落伤害事件,取消飞行玩家的摔落伤害 // 注册摔落伤害事件,取消飞行玩家的摔落伤害
ServerLivingEntityEvents.ALLOW_DAMAGE.register((entity, damageSource, amount) -> { ServerLivingEntityEvents.ALLOW_DAMAGE.register((entity, damageSource, amount) -> {
if (entity instanceof ServerPlayerEntity player) { if (entity instanceof ServerPlayerEntity player) {
if (flyingPlayers.contains(player.getUuid())) { if (isFlying(player.getUuid())) {
// 如果是摔落伤害,取消它 // 如果是摔落伤害,取消它
if (damageSource.isOf(DamageTypes.FALL)) { if (damageSource.isOf(DamageTypes.FALL)) {
return false; // 返回false取消伤害 return false; // 返回false取消伤害
@@ -73,28 +118,111 @@ public class fly {
} }
return true; // 其他情况正常受到伤害 return true; // 其他情况正常受到伤害
}); });
// 死亡重生后重新应用飞行状态(原版重生流程会重置能力)
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> {
restoreFlyState(newPlayer);
});
}
private static boolean isFlying(UUID playerId) {
PlayerFlightState state = flightStates.get(playerId);
return state != null && state.flying;
}
private static PlayerFlightState getOrCreateState(UUID playerId) {
return flightStates.computeIfAbsent(playerId, id -> new PlayerFlightState());
} }
private static void enableFly(ServerPlayerEntity player) { private static void enableFly(ServerPlayerEntity player) {
flyingPlayers.add(player.getUuid()); PlayerFlightState state = getOrCreateState(player.getUuid());
state.flying = true;
player.getAbilities().allowFlying = true; player.getAbilities().allowFlying = true;
player.getAbilities().flying = true; player.getAbilities().flying = true;
player.getAbilities().setFlySpeed(state.flySpeed);
player.sendAbilitiesUpdate(); player.sendAbilitiesUpdate();
saveStates();
} }
private static void disableFly(ServerPlayerEntity player) { private static void disableFly(ServerPlayerEntity player) {
flyingPlayers.remove(player.getUuid()); PlayerFlightState state = getOrCreateState(player.getUuid());
state.flying = false;
player.getAbilities().allowFlying = false; player.getAbilities().allowFlying = false;
player.getAbilities().flying = false; player.getAbilities().flying = false;
player.sendAbilitiesUpdate(); player.sendAbilitiesUpdate();
saveStates();
} }
// 玩家加入时恢复飞行状态 // 玩家加入/重生时恢复飞行状态(是否飞行 + 飞行速度)
public static void restoreFlyState(ServerPlayerEntity player) { public static void restoreFlyState(ServerPlayerEntity player) {
if (flyingPlayers.contains(player.getUuid())) { PlayerFlightState state = flightStates.get(player.getUuid());
if (state == null) {
return;
}
// 飞行速度始终恢复,是否飞行按保存的状态恢复
player.getAbilities().setFlySpeed(state.flySpeed);
if (state.flying) {
player.getAbilities().allowFlying = true; player.getAbilities().allowFlying = true;
player.getAbilities().flying = true; player.getAbilities().flying = true;
player.sendAbilitiesUpdate(); }
player.sendAbilitiesUpdate();
}
// ---------- 持久化 ----------
private static Path dataFile() {
return FabricLoader.getInstance().getConfigDir().resolve("openfly_flight_data.json");
}
private static void loadStates() {
if (loaded) {
return;
}
loaded = true;
try {
Path path = dataFile();
if (!Files.exists(path)) {
return;
}
try (Reader reader = Files.newBufferedReader(path)) {
Map<String, PlayerFlightState> data = GSON.fromJson(reader, new TypeToken<Map<String, PlayerFlightState>>() {
}.getType());
if (data != null) {
for (Map.Entry<String, PlayerFlightState> entry : data.entrySet()) {
try {
flightStates.put(UUID.fromString(entry.getKey()), entry.getValue());
} catch (IllegalArgumentException ignored) {
// 忽略无效的 UUID
}
}
}
}
OpenFly.LOGGER.info("OpenFly 已加载 {} 名玩家的飞行状态", flightStates.size());
} catch (IOException e) {
OpenFly.LOGGER.error("OpenFly 读取飞行状态文件失败", e);
} }
} }
private static void saveStates() {
try {
Path path = dataFile();
Files.createDirectories(path.getParent());
// 以 UUID 字符串作为 key 序列化,避免依赖 Gson 的 UUID 适配器
Map<String, PlayerFlightState> data = new HashMap<>();
for (Map.Entry<UUID, PlayerFlightState> entry : flightStates.entrySet()) {
data.put(entry.getKey().toString(), entry.getValue());
}
try (Writer writer = Files.newBufferedWriter(path)) {
GSON.toJson(data, writer);
}
} catch (IOException e) {
OpenFly.LOGGER.error("OpenFly 保存飞行状态文件失败", e);
}
}
// 单个玩家的飞行状态
public static class PlayerFlightState {
public boolean flying = false;
public float flySpeed = DEFAULT_FLY_SPEED;
}
} }
@@ -1,7 +1,9 @@
package top.gtb520.java.mods.openfly; package top.gtb520.java.mods.openfly;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -26,9 +28,21 @@ public class OpenFly implements ModInitializer {
// 注册飞行命令和摔落伤害事件 // 注册飞行命令和摔落伤害事件
fly.register(); fly.register();
// 玩家加入时恢复飞行状态 // 玩家加入时恢复飞行状态(是否飞行 + 飞行速度)
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
fly.restoreFlyState(handler.getPlayer()); ServerPlayerEntity player = handler.getPlayer();
fly.restoreFlyState(player);
// 延迟一 tick 再应用一次,避免登录流程后续覆盖玩家能力
server.execute(() -> {
if (!player.isDisconnected()) {
fly.restoreFlyState(player);
}
});
});
// 死亡重生后重新应用飞行状态(原版重生流程会重置能力)
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> {
fly.restoreFlyState(newPlayer);
}); });
LOGGER.info("OpenFly Mod 加载完成!"); LOGGER.info("OpenFly Mod 加载完成!");