diff --git a/README.md b/README.md index b411769..63755f7 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ ## 功能特性 - 🕊️ **自由飞行** — 通过命令开启/关闭创造模式飞行能力 +- ⚡ **飞行速度调节** — 通过命令在 0.01–0.15 范围内自定义飞行速度 - 🛡️ **摔落保护** — 飞行状态下免疫摔落伤害 -- 💾 **状态持久化** — 重新加入服务器时自动恢复飞行状态 +- 💾 **状态持久化** — 飞行状态与速度保存到配置文件,退出重进或服务器重启后自动恢复 - 👥 **多玩家支持** — 管理员可为其他玩家切换飞行状态 ## 环境要求 @@ -36,12 +37,24 @@ - 如果当前未飞行,执行后**开启飞行** - 如果当前正在飞行,执行后**关闭飞行** -### `/fly ` +### `/fly speed <0.01-0.15>` + +设置自己的飞行速度,范围为 0.01 到 0.15(默认 0.05)。所有玩家均可执行。 + +``` +/fly speed 0.10 +``` + +- 数值越大飞行越快,0.15 为允许的最大速度 +- 设置后立即生效,无需重新开启飞行 +- 速度会与飞行状态一起保存,退出重进后仍然保留 + +### `/fly player ` 切换指定玩家的飞行状态。需要 OP 权限(权限等级 2)。 ``` -/fly Steve +/fly player Steve ``` - 如果目标玩家未飞行,执行后**为其开启飞行** @@ -59,17 +72,25 @@ - **双击空格** — 切换飞行/普通状态 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 Alex` — 关闭 Alex 的飞行 +- `/fly player Steve` — 为 Steve 开启飞行 +- `/fly player Alex` — 关闭 Alex 的飞行 ### 摔落保护 @@ -77,13 +98,13 @@ ### 重连恢复 -如果玩家在飞行状态下断开连接,重新加入服务器时飞行状态会自动恢复。 +玩家的飞行状态(是否飞行)与飞行速度会持久化保存。退出游戏后重新加入,甚至服务器重启,都会自动恢复上次的飞行状态与速度。 ## FAQ ### Q: 飞行状态会一直保留吗? -A: 飞行状态在服务器运行期间持续有效。如果你关闭了飞行、或者服务器重启,飞行状态会被重置。但如果你是断开重连,飞行状态会自动恢复。 +A: 会。飞行状态(是否飞行)与飞行速度会保存到服务端的 `config/openfly_flight_data.json` 文件。玩家退出重进、服务器重启后都会自动恢复上次的状态。 ### Q: 飞行时还会受到摔落伤害吗? @@ -91,7 +112,7 @@ A: 不会。飞行状态下免疫所有摔落伤害,关闭飞行后恢复正 ### Q: 普通玩家可以使用 `/fly` 吗? -A: 可以。`/fly` 命令对所有玩家开放,无需 OP 权限。但 `/fly ` 需要 OP 权限。 +A: 可以。`/fly` 与 `/fly speed` 命令对所有玩家开放,无需 OP 权限。但 `/fly player ` 需要 OP 权限。 ### Q: 这个模组需要安装在客户端还是服务端? diff --git a/gradle.properties b/gradle.properties index 1787ff7..0ae8094 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ loader_version=0.19.5 loom_version=1.18-SNAPSHOT # Mod Properties -version=1.0.1 +version=1.2.0-1.21.11 group=top.gtb520.java.mods.openfly # Dependencies diff --git a/src/main/java/top/gtb520/java/mods/openfly/Commands/fly.java b/src/main/java/top/gtb520/java/mods/openfly/Commands/fly.java index b1051cb..fa2b481 100644 --- a/src/main/java/top/gtb520/java/mods/openfly/Commands/fly.java +++ b/src/main/java/top/gtb520/java/mods/openfly/Commands/fly.java @@ -1,24 +1,48 @@ 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.arguments.FloatArgumentType; 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.ServerPlayerEvents; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.command.DefaultPermissions; import net.minecraft.command.argument.EntityArgumentType; import net.minecraft.entity.damage.DamageTypes; import net.minecraft.server.command.CommandManager; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; +import top.gtb520.java.mods.openfly.OpenFly; -import java.util.HashSet; -import java.util.Set; +import java.io.IOException; +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; public class fly { - // 记录开启了飞行的玩家UUID - private static final Set 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 flightStates = new HashMap<>(); + private static boolean loaded = false; public static void register() { + loadStates(); + // 注册命令 CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { dispatcher.register( @@ -32,7 +56,7 @@ public class fly { } UUID playerId = player.getUuid(); - if (flyingPlayers.contains(playerId)) { + if (isFlying(playerId)) { disableFly(player); context.getSource().sendFeedback(() -> Text.literal("已禁用飞行"), false); } else { @@ -41,31 +65,52 @@ public class fly { } return Command.SINGLE_SUCCESS; }) - // /fly - 切换指定玩家的飞行状态(需要OP权限) - .then(CommandManager.argument("player", EntityArgumentType.player()) - .requires(source -> source.getPermissions().hasPermission(DefaultPermissions.GAMEMASTERS)) - .executes(context -> { - ServerPlayerEntity targetPlayer = EntityArgumentType.getPlayer(context, "player"); - UUID playerId = targetPlayer.getUuid(); + // /fly speed <0.01-0.15> - 设置自己的飞行速度(所有玩家可用) + .then(CommandManager.literal("speed") + .then(CommandManager.argument("speed", FloatArgumentType.floatArg(MIN_FLY_SPEED, MAX_FLY_SPEED)) + .executes(context -> { + ServerPlayerEntity player = context.getSource().getPlayer(); + if (player == null) { + context.getSource().sendFeedback(() -> Text.literal("该命令只能由玩家执行"), false); + return 0; + } - if (flyingPlayers.contains(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; - })) + float speed = FloatArgumentType.getFloat(context, "speed"); + PlayerFlightState state = getOrCreateState(player.getUuid()); + state.flySpeed = speed; + player.getAbilities().setFlySpeed(speed); + player.sendAbilitiesUpdate(); + saveStates(); + context.getSource().sendFeedback( + () -> Text.literal("飞行速度已设置为 " + String.format("%.2f", speed)), false); + return Command.SINGLE_SUCCESS; + }))) + // /fly player - 切换指定玩家的飞行状态(需要OP权限) + .then(CommandManager.literal("player") + .requires(source -> source.getPermissions().hasPermission(DefaultPermissions.GAMEMASTERS)) + .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) -> { if (entity instanceof ServerPlayerEntity player) { - if (flyingPlayers.contains(player.getUuid())) { + if (isFlying(player.getUuid())) { // 如果是摔落伤害,取消它 if (damageSource.isOf(DamageTypes.FALL)) { return false; // 返回false取消伤害 @@ -74,28 +119,111 @@ public class fly { } 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) { - flyingPlayers.add(player.getUuid()); + PlayerFlightState state = getOrCreateState(player.getUuid()); + state.flying = true; player.getAbilities().allowFlying = true; player.getAbilities().flying = true; + player.getAbilities().setFlySpeed(state.flySpeed); player.sendAbilitiesUpdate(); + saveStates(); } private static void disableFly(ServerPlayerEntity player) { - flyingPlayers.remove(player.getUuid()); + PlayerFlightState state = getOrCreateState(player.getUuid()); + state.flying = false; player.getAbilities().allowFlying = false; player.getAbilities().flying = false; player.sendAbilitiesUpdate(); + saveStates(); } - // 玩家加入时恢复飞行状态 + // 玩家加入/重生时恢复飞行状态(是否飞行 + 飞行速度) 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().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 data = GSON.fromJson(reader, new TypeToken>() { + }.getType()); + if (data != null) { + for (Map.Entry 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 data = new HashMap<>(); + for (Map.Entry 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; + } } diff --git a/src/main/java/top/gtb520/java/mods/openfly/OpenFly.java b/src/main/java/top/gtb520/java/mods/openfly/OpenFly.java index 9d95d51..7c3efcb 100644 --- a/src/main/java/top/gtb520/java/mods/openfly/OpenFly.java +++ b/src/main/java/top/gtb520/java/mods/openfly/OpenFly.java @@ -1,7 +1,9 @@ package top.gtb520.java.mods.openfly; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.Identifier; import org.slf4j.Logger; @@ -26,9 +28,21 @@ public class OpenFly implements ModInitializer { // 注册飞行命令和摔落伤害事件 fly.register(); - // 玩家加入时恢复飞行状态 + // 玩家加入时恢复飞行状态(是否飞行 + 飞行速度) 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 加载完成!");