From b1a611a3e66ad14aad3a6342b1b9e9a87a820620 Mon Sep 17 00:00:00 2001 From: iceBear67 Date: Tue, 9 Jul 2024 18:13:03 +0800 Subject: [PATCH] init. --- README.md | 21 +++ assets/img.png | Bin 0 -> 34126 bytes build.gradle | 17 ++- src/main/java/io/ib67/sfcraft/Commands.java | 121 ++++++++++++---- src/main/java/io/ib67/sfcraft/Helper.java | 31 +++++ src/main/java/io/ib67/sfcraft/Listener.java | 131 +++++++++++++++--- src/main/java/io/ib67/sfcraft/Permission.java | 49 +++++++ src/main/java/io/ib67/sfcraft/SFConfig.java | 6 - src/main/java/io/ib67/sfcraft/SFConsts.java | 21 +++ src/main/java/io/ib67/sfcraft/SFCraft.java | 123 +++++++++------- .../ib67/sfcraft/access/AccessController.java | 18 --- .../io/ib67/sfcraft/access/GrantedEntry.java | 8 -- .../access/impl/SimpleAccessController.java | 42 ------ .../java/io/ib67/sfcraft/config/SFConfig.java | 19 +++ .../io/ib67/sfcraft/geoip/GeoIPService.java | 9 +- .../mixin/server/LegacyQueryHandlerMixin.java | 42 ++++++ .../mixin/server/PlayerEntityMixin.java | 18 +++ .../ServerHandshakeNetworkHandlerMixin.java | 24 ++++ .../ServerLoginNetworkHandlerMixin.java | 2 +- src/main/resources/sfcraft.mixins.json | 3 + 20 files changed, 528 insertions(+), 177 deletions(-) create mode 100644 README.md create mode 100644 assets/img.png create mode 100644 src/main/java/io/ib67/sfcraft/Helper.java create mode 100644 src/main/java/io/ib67/sfcraft/Permission.java delete mode 100644 src/main/java/io/ib67/sfcraft/SFConfig.java create mode 100644 src/main/java/io/ib67/sfcraft/SFConsts.java delete mode 100644 src/main/java/io/ib67/sfcraft/access/AccessController.java delete mode 100644 src/main/java/io/ib67/sfcraft/access/GrantedEntry.java delete mode 100644 src/main/java/io/ib67/sfcraft/access/impl/SimpleAccessController.java create mode 100644 src/main/java/io/ib67/sfcraft/config/SFConfig.java create mode 100644 src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java create mode 100644 src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java create mode 100644 src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed999f0 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# SFCraft + +本仓库为 S.F Craft 服务器的功能 Mod, S.F Craft 是一个邀请制的纯原版生存服务器。 +本插件专门为 SFC 定制,因此您可能需要花费大多功夫来将其用在你的服务器上(比如修改大量的硬编码字符串,强制启用的功能,等等),因此,并不建议这么做。 + +本仓库开源供有能力的玩家贡献。 + +# Features + +- 允许指定离线玩家进入正版服务器,与白名单兼容 (`/addwl`) + 需要注意的是,如果使用此功能添加了正版玩家的名字,那么正版玩家将会以离线用户的 UUID 游玩,这会导致皮肤不显示等问题。 +- 限制性的 `/back` 命令 + 切合服务器的需要,本服务器的 `/back` 命令仅在死亡地点半径 100 内有人的情况下才可传送,且传送到附近玩家的位置。 +- 更新日志速览 + 新来的玩家将会发放小册子,简要概述服务器特性。 + 该书内容从 `sfcraft/update.log` 生成,每 128 字符一张纸。 +- 防沉迷 + 保护每一个玩家的下午,支持自动识别时区,需要下载 GeoIP 数据库 (45MB)。 + ![img.png](assets/img.png) +- 人文关怀 + 对于一些年龄比较小的玩家还有一些不太熟悉 MC 的玩家,我们特地设计了一些赦免权,例如无限制的 `/back` 命令。 \ No newline at end of file diff --git a/assets/img.png b/assets/img.png new file mode 100644 index 0000000000000000000000000000000000000000..0d26e71df89e93984062dd87cbcc70f786f54e49 GIT binary patch literal 34126 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 P1bnF}X(?97n+N|tK?;6X literal 0 HcmV?d00001 diff --git a/build.gradle b/build.gradle index 87968dd..717823c 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'fabric-loom' version '1.6-SNAPSHOT' id 'maven-publish' id("io.freefair.lombok") version "8.6" + id("com.github.johnrengelman.shadow") version "8.1.1" } version = project.mod_version @@ -28,7 +29,21 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - implementation 'com.maxmind.geoip2:geoip2:4.2.0' + shadow('com.maxmind.geoip2:geoip2:4.2.0') +} +configurations.implementation.extendsFrom(configurations.shadow) + +shadowJar { + configurations = [ project.configurations.shadow ] + exclude("META-INF") +} + +remapJar { + // wait until the shadowJar is done + dependsOn(shadowJar) + mustRunAfter(shadowJar) + // Set the input jar for the task. Here use the shadow Jar that include the .class of the transitive dependency + inputFile = file(shadowJar.archivePath) } processResources { diff --git a/src/main/java/io/ib67/sfcraft/Commands.java b/src/main/java/io/ib67/sfcraft/Commands.java index a77a2b2..4279a77 100644 --- a/src/main/java/io/ib67/sfcraft/Commands.java +++ b/src/main/java/io/ib67/sfcraft/Commands.java @@ -1,46 +1,113 @@ package io.ib67.sfcraft; -import io.ib67.sfcraft.access.AccessController; +import com.mojang.brigadier.context.CommandContext; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.WhitelistEntry; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.dedicated.MinecraftDedicatedServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; import net.minecraft.text.Text; import net.minecraft.text.TextColor; +import net.minecraft.util.Colors; import net.minecraft.util.StringHelper; +import net.minecraft.util.Uuids; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Field; import java.net.InetSocketAddress; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; public final class Commands { - @RequiredArgsConstructor - static class AccessControl { - private final AccessController controller; - public void grantAccess(ServerCommandSource sender, String name, @Nullable String ip) { - if (!sender.hasPermissionLevel(2)) { - sender.sendMessage(Text.of("Permission denied.")); - return; - } - if (!StringHelper.isValidPlayerName(name)) { - sender.sendMessage(Text.of("Invalid player name.")); - return; - } - if (ip == null) { - sender.sendMessage(Text.of("You're granting ANY access to player " + name + "!")); - controller.grantAccess(name, null); - } else { - controller.grantAccess(name, ip); - } - sender.sendMessage(Text.of(name + ": Access granted.")); - } + public Commands() { + ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); + } - public void revokeAccess(ServerCommandSource sender, String name) { - controller.revokeAccess(name); - sender.sendMessage(Text.of(name + ": Permission revoked.")); - } + private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { + serverPlayNetworkHandler.getPlayer().removeCommandTag(SFConsts.SPECIAL_SUDO); + } - public void listAccesses(ServerCommandSource sender) { - controller.getEntries().forEach(it -> sender.sendMessage(Text.of(it.name() + ": " + (it.condition() == null ? "ANY" : it.condition())))); + public int addPlayerOffline(ServerCommandSource source, String player) { + player = player.trim(); + var pm = source.getServer().getPlayerManager(); + var wl = pm.getWhitelist(); + if (List.of(wl.getNames()).contains(player)) { + source.sendError(Text.of(player + " is already whitelisted or this id is conflict with a online user.")); + return 0; } + var profile = Uuids.getOfflinePlayerProfile(player); + wl.add(new WhitelistEntry(profile)); + source.sendMessage(Text.of("[" + profile.getName() + "/" + profile.getId() + "]" + " is added!")); + return 0; + } + + public int onBack(CommandContext it) { + var player = it.getSource().getPlayer(); + var pos = player.getLastDeathPos().get(); + var wld = player.server.getWorld(pos.dimension()); + var _pos = pos.pos(); + if (wld == null) return 0; + var nearby = wld.getClosestPlayer(_pos.getX(), _pos.getY(), _pos.getZ(), 20, true); + if (nearby != null) { + _pos = nearby.getBlockPos(); + player.teleport(wld, _pos.getX(), _pos.getY(), _pos.getZ(), 0, 0); + } else { + if (SFConsts.UNLIMITED_COMMAND_BACK.hasPermission(player)) { + player.teleport(wld, pos.pos().getX(), pos.pos().getY(), pos.pos().getZ(), 0, 0); + return 0; + } + player.sendMessage(Text.of("周围没有玩家。").copy().withColor(Colors.RED)); + } + return 0; + } + + public int unblockServer(CommandContext ctx) { + var state = SFCraft.getInstance().getConfig().disableTemporarily; + SFCraft.getInstance().getConfig().disableTemporarily = !state; + ctx.getSource().sendMessage(Text.of("Current state: " + !state)); + return 0; + } + + public int enterSudoMode(CommandContext serverCommandSourceCommandContext) { + var player = serverCommandSourceCommandContext.getSource().getPlayer(); + if (player.getCommandTags().contains(SFConsts.SPECIAL_SUDO)) { + player.removeCommandTag(SFConsts.SPECIAL_SUDO); + player.sendMessage(Text.of("Sudo is off.").copy().withColor(Colors.GREEN)); + } else { + player.addCommandTag(SFConsts.SPECIAL_SUDO); + player.sendMessage(Text.of("Sudo is on.").copy().withColor(Colors.GREEN)); + } + return 0; + } + + @SneakyThrows + public int listPerms(CommandContext serverCommandSourceCommandContext) { + var src = serverCommandSourceCommandContext.getSource(); + src.sendMessage(Text.of("Permissions: ").copy().withColor(Colors.GREEN)); + for (Field declaredField : SFConsts.class.getDeclaredFields()) { + if (Permission.class.isAssignableFrom(declaredField.getType())) { + var permission = (Permission) declaredField.get(null); + src.sendMessage(Text.of(" - " + permission.key() + " " + (permission.byDefault() ? "(default)" : ""))); + } + } + return 0; + } + + public int listOffline(CommandContext ctx) { + var source = ctx.getSource(); + var server = source.getServer(); + var wl = server.getPlayerManager().getWhitelist(); + var names = wl.getNames(); + source.sendMessage(Text.of("Offline Users:").copy().withColor(Colors.GREEN)); + var r = Stream.of(names).filter(name -> wl.isAllowed(Uuids.getOfflinePlayerProfile(name))).collect(Collectors.joining(", ")); + source.sendMessage(Text.of(r.isEmpty() ? "No offline users found in whitelist." : r)); + return 0; } } diff --git a/src/main/java/io/ib67/sfcraft/Helper.java b/src/main/java/io/ib67/sfcraft/Helper.java new file mode 100644 index 0000000..6311710 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/Helper.java @@ -0,0 +1,31 @@ +package io.ib67.sfcraft; + +import com.maxmind.geoip2.exception.GeoIp2Exception; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.net.InetSocketAddress; + +public class Helper { + public static boolean canBack(ServerPlayerEntity player) { + if (!SFConsts.COMMAND_BACK.hasPermission(player)) return false; + var pos = player.getLastDeathPos().get(); + var wld = player.server.getWorld(pos.dimension()); + var _pos = pos.pos(); + if (wld == null) return false; + var nearby = wld.getClosestPlayer(_pos.getX(), _pos.getY(), _pos.getZ(), 20, true); + return nearby != null; + } + + public static String getVersionString(InetSocketAddress address) { + try { + var clock = SFCraft.getInstance().getGeoIPService().clockOf(address.getAddress()); + var config = SFCraft.getInstance().getConfig(); + if (config.isClosed(clock)) { + return "防沉迷: " + config.maintainceStartHour + "~" + config.maintainceEndHour; + } + } catch (GeoIp2Exception ignored) { + return "Not Available!"; + } + return null; + } +} diff --git a/src/main/java/io/ib67/sfcraft/Listener.java b/src/main/java/io/ib67/sfcraft/Listener.java index 8e7e7f8..eebe5a4 100644 --- a/src/main/java/io/ib67/sfcraft/Listener.java +++ b/src/main/java/io/ib67/sfcraft/Listener.java @@ -1,17 +1,32 @@ package io.ib67.sfcraft; -import com.maxmind.geoip2.record.City; +import com.maxmind.geoip2.exception.GeoIp2Exception; import lombok.SneakyThrows; -import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.WrittenBookContentComponent; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; import net.minecraft.network.ClientConnection; -import net.minecraft.network.DisconnectionInfo; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerMetadata; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.RawFilteredPair; import net.minecraft.text.Text; +import net.minecraft.util.Uuids; import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.time.Clock; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.function.Consumer; public class Listener { + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy/MM/dd"); private final SFCraft core; public Listener(SFCraft core) { @@ -20,22 +35,104 @@ public class Listener { @SneakyThrows public boolean onPlayerLogin(String currentPlayer, ClientConnection connection, Consumer disconnect) { + var wl = core.getServer().getPlayerManager().getWhitelist(); + var offlineProfile = Uuids.getOfflinePlayerProfile(currentPlayer); if (connection.getAddress() instanceof InetSocketAddress address) { - var clock = core.getGeoIPService().clockOf(address.getAddress()); - var hourOfDay = ZonedDateTime.now(clock).getHour(); - var startHr = SFCraft.getInstance().getConfig().maintainceStartHour; - var endHr = SFCraft.getInstance().getConfig().maintainceEndHour; - if (startHr > hourOfDay && hourOfDay > endHr) { - final var msg = "服务器尚未开放\n服务器维护时间: %1$d~%2$d\n你的下午不值得浪费在游戏上,晚点再来吧!"; - disconnect.accept(Text.of(String.format(msg, startHr, endHr))); - return false; - } - var canJoin = core.getAccessController().checkAccess(currentPlayer, address.getAddress().getHostAddress()); - if (!canJoin) { - final var msg = ""; - disconnect.accept(Text.of(String.format(msg, startHr, endHr))); + try { + var clock = core.getGeoIPService().clockOf(address.getAddress()); + var config = core.getConfig(); + if (config.isClosed(clock) && !"iceBear67".equals(currentPlayer)) { + final var msg = "服务器尚未开放\n服务器维护时间: %1$d~%2$d\n你的下午不值得浪费在游戏上,晚点再来吧!"; + disconnect.accept(Text.of(String.format(msg, config.maintainceStartHour, config.maintainceEndHour))); + } + } catch (Exception ignored) { + System.err.println("Failed to locate " + address.getAddress()); } } - return true; + return wl.isAllowed(offlineProfile); + } + + public void onPlayerDeath(ServerPlayerEntity player, DamageSource damageSource) { + if (Helper.canBack(player)) { + player.sendMessage(Text.of("Tip: 死亡地点附近有玩家,可以使用 /back 传送到他们那里。(即使在对方也死亡的状态下)")); + } + } + + public void onPlayerJoin(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender + packetSender, MinecraftServer minecraftServer) { + var player = serverPlayNetworkHandler.getPlayer(); + if (!player.getCommandTags().contains("has_joined_before")) { + player.addCommandTag("has_joined_before"); + player.sendMessage(Text.of("欢迎加入 SaltedFish Club Server!")); + player.sendMessage(Text.of("您受本服务器成员邀请并第一次加入本服务器,以下是一些注意事项:")); + player.sendMessage(Text.of(" 1. 服务器内有 base,但我们也鼓励您自立门户")); + player.sendMessage(Text.of(" 2. 服务器 QQ 群: (qq)")); + player.sendMessage(Text.of(" 3. 下午 " + SFCraft.getInstance().getConfig().maintainceStartHour + " ~ " + SFCraft.getInstance().getConfig().maintainceEndHour + " 为服务器维护时间(防沉迷),在这段时间内无法加入服务器。")); + player.sendMessage(Text.of("该时间由您 IP 属地的时区决定。我们不希望您的下午仅仅荒废在服务器上。")); + player.sendMessage(Text.of("如果您想邀请朋友加入服务器,可以向管理员提出申请。本服务器为正版服,但是离线玩家也可以加入(需备注)。")); + player.sendMessage(Text.of("为了账号安全起见,请尽量使用正版账号登录后加入。此外,正/盗同名将会被视为两个不同的玩家存档,数据不互通。")); + player.giveItemStack(generateHelperBook(core.getUpdateLog(), player)); + unlockRecipe(player); + } + } + + private ItemStack generateHelperBook(String updateLog, ServerPlayerEntity player) { + Clock clock = null; + if (player.networkHandler.getConnectionAddress() instanceof InetSocketAddress addr) { + try { + clock = SFCraft.getInstance().getGeoIPService().clockOf(addr.getAddress()); + } catch (GeoIp2Exception e) { + clock = Clock.systemDefaultZone(); + } + } + var book = new ItemStack(Items.WRITTEN_BOOK); + var updLog = new ArrayList(); + updLog.add("本书修订截至至您加入服务器的那一刻 (" + FMT.format(ZonedDateTime.now(clock)) + ")\n" + "更多内容请查阅群公告"); + var pages = updateLog.length() / 100; + for (int i = 0; i < pages; i++) { + updLog.add(updateLog.substring(i * 100, (i + 1) * 100)); + } + updLog.add(updateLog.substring(pages * 100, updateLog.length())); + var component = new WrittenBookContentComponent( + RawFilteredPair.of("特性列表"), + "icybear & You", + 1, + updLog.stream().map(it -> RawFilteredPair.of(Text.of(it))).toList(), + false + ); + book.set(DataComponentTypes.WRITTEN_BOOK_CONTENT, component); + return book; + } + + private void unlockRecipe(ServerPlayerEntity player) { + var recipe = player.getRecipeBook(); + recipe.unlockRecipes(player.server.getRecipeManager().values(), player); + } + + private final Set motdRequestedIps = new HashSet<>(); + + public ServerMetadata onMotdPing(MinecraftServer instance, ClientConnection connection) { + var ip = connection.getAddressAsString(false); + var cached = motdRequestedIps.contains(ip); + if (!cached) { + motdRequestedIps.add(ip); + } + var originalMetadata = instance.getServerMetadata(); + if (originalMetadata == null) return null; + String version = null; + if (connection.getAddress() instanceof InetSocketAddress address) { + version = Helper.getVersionString(address); + } + if (version != null) { + return new ServerMetadata( + originalMetadata.description(), + originalMetadata.players(), + Optional.of(new ServerMetadata.Version(version, 0)), + cached ? Optional.empty() : originalMetadata.favicon(), + originalMetadata.secureChatEnforced() + ); + } else { + return originalMetadata; + } } } diff --git a/src/main/java/io/ib67/sfcraft/Permission.java b/src/main/java/io/ib67/sfcraft/Permission.java new file mode 100644 index 0000000..28bbfef --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/Permission.java @@ -0,0 +1,49 @@ +package io.ib67.sfcraft; + +import lombok.Getter; +import net.minecraft.entity.Entity; + +import java.util.Objects; + +/** + * 基于 Command Tags 实现的权限系统 + * 权限节点通常以 `.` 切割并且总是在 `sfcraft` 分类下,例如 `sfcraft.back` + * 用 `-权限节点` 表示禁止使用 + * + * @param 对象 + */ +public record Permission(String key, boolean byDefault) { + /** + * @param key 权限节点 + */ + public Permission(String key, boolean byDefault) { + this.key = Objects.requireNonNull(key).trim().toLowerCase(); + this.byDefault = byDefault; + } + + public boolean hasPermission(T t) { + if (t.getCommandTags().contains(SFConsts.SPECIAL_SUDO)) return true; + if (byDefault) { + return !t.getCommandTags().contains("-" + key); + } + return t.getCommandTags().contains(key); + } + + public void grant(T t) { + if (!hasPermission(t)) { + t.getCommandTags().remove("-" + key); + if (!byDefault) { + t.addCommandTag(key); + } + } + } + + public void revoke(T t) { + if (hasPermission(t)) { + t.removeCommandTag(key); + if (byDefault) { + t.addCommandTag("-" + key); + } + } + } +} diff --git a/src/main/java/io/ib67/sfcraft/SFConfig.java b/src/main/java/io/ib67/sfcraft/SFConfig.java deleted file mode 100644 index eb30902..0000000 --- a/src/main/java/io/ib67/sfcraft/SFConfig.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.ib67.sfcraft; - -public class SFConfig { - public int maintainceStartHour = 17; - public int maintainceEndHour = 1; -} diff --git a/src/main/java/io/ib67/sfcraft/SFConsts.java b/src/main/java/io/ib67/sfcraft/SFConsts.java new file mode 100644 index 0000000..be0ef5c --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/SFConsts.java @@ -0,0 +1,21 @@ +package io.ib67.sfcraft; + +import lombok.experimental.UtilityClass; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; + +@UtilityClass +public class SFConsts { + public static final String SPECIAL_SUDO = "sfcraft.special.state.sudo"; + public static final Permission COMMAND_BACK = ofSFCPermission("command.home", true); + public static final Permission UNLIMITED_COMMAND_BACK = ofSFCPermission("command.home.unlimited", true); + + public static final Permission COMMAND_ADDWL = ofSFCPermission("command.addwl", false); + public static final Permission COMMAND_UNBLOCKSERVER = ofSFCPermission("command.unblockserver", false); + public static final Permission COMMAND_LISTOFFLINE = ofSFCPermission("command.listoffline", false); + public static final Permission COMMAND_LISTPERM = ofSFCPermission("command.listperm", false); + + public static Permission ofSFCPermission(String key, boolean byDefault) { + return new Permission<>("sfcraft." + key, byDefault); + } +} diff --git a/src/main/java/io/ib67/sfcraft/SFCraft.java b/src/main/java/io/ib67/sfcraft/SFCraft.java index 2c81532..986f383 100644 --- a/src/main/java/io/ib67/sfcraft/SFCraft.java +++ b/src/main/java/io/ib67/sfcraft/SFCraft.java @@ -5,15 +5,14 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import io.ib67.sfcraft.access.AccessController; -import io.ib67.sfcraft.access.GrantedEntry; -import io.ib67.sfcraft.access.impl.SimpleAccessController; +import io.ib67.sfcraft.config.SFConfig; import io.ib67.sfcraft.geoip.GeoIPService; import lombok.Getter; import lombok.SneakyThrows; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.CommandManager; @@ -21,93 +20,111 @@ import net.minecraft.server.command.ServerCommandSource; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.stream.Collectors; public class SFCraft implements ModInitializer { @Getter private SFConfig config; @Getter - private static Listener listener; + private Listener listener; @Getter private static SFCraft instance; @Getter private GeoIPService geoIPService; @Getter - private AccessController accessController; - @Getter private Path root; + @Getter + private MinecraftServer server; + @Getter + private String motd; + @Getter + private String updateLog; @Override @SneakyThrows public void onInitialize() { instance = this; - listener = new Listener(this); root = Path.of("sfcraft"); if (Files.notExists(root)) { Files.createDirectory(root); } - loadConfig(); + listener = new Listener(this); + config = loadConfig(); + motd = loadMotd(); + updateLog = loadUpdateLog(); geoIPService = new GeoIPService(); - accessController = loadAccessController(); + + ServerPlayConnectionEvents.JOIN.register(listener::onPlayerJoin); CommandRegistrationCallback.EVENT.register(this::registerCommands); + ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStarted); + } + + private void onServerStarted(MinecraftServer minecraftServer) { + server = minecraftServer; + server.setMotd(motd); } @SneakyThrows - private void loadConfig() { + private String loadUpdateLog() { + var log = root.resolve("update.log"); + if (Files.notExists(log)) { + return "他没写呢"; + } + return Files.readString(log); + } + + @SneakyThrows + private String loadMotd() { + var motd = root.resolve("motd.txt"); + if (Files.notExists(motd)) { + Files.writeString(motd, "§bSaltedFish Club Server"); + } + return Files.readString(motd); + } + + @SneakyThrows + private SFConfig loadConfig() { var gson = new Gson(); var cfgFile = root.resolve("config.json"); if (Files.notExists(cfgFile)) { Files.writeString(cfgFile, gson.toJson(new SFConfig())); - config = new SFConfig(); - return; + return new SFConfig(); } - config = gson.fromJson(Files.readString(cfgFile), SFConfig.class); - } - - @SneakyThrows - private AccessController loadAccessController() { - var file = root.resolve("access.txt"); - ServerLifecycleEvents.SERVER_STOPPING.register(this::saveAccessRules); - if (Files.notExists(file)) { - return new SimpleAccessController(List.of()); - } - return new SimpleAccessController(Files.readAllLines(file).stream().map(it -> it.split(":")).filter(it -> it.length == 2).map(it -> new GrantedEntry(it[0].trim(), it[1].trim())).toList()); - } - - @SneakyThrows - private void saveAccessRules(MinecraftServer minecraftServer) { - var file = root.resolve("access.txt"); - Files.writeString(file, accessController.getEntries().stream().map(i -> i.name() + ":" + i.condition()).collect(Collectors.joining("\n"))); + return gson.fromJson(Files.readString(cfgFile), SFConfig.class); } private void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) { - var accessControl = new Commands.AccessControl(accessController); + var handler = new Commands(); + + // 一般非玩家实体都有 >= 2 的 PermissionLevel,因此不需要额外检查。 dispatcher.register( - LiteralArgumentBuilder.literal("access") - .requires(it -> it.hasPermissionLevel(2)) - .then(LiteralArgumentBuilder.literal("grant") - .then(RequiredArgumentBuilder.argument("name", StringArgumentType.string()).executes(it -> { - accessControl.grantAccess(it.getSource(), it.getArgument("name", String.class), null); - return 0; - }).then(RequiredArgumentBuilder.argument("ip", StringArgumentType.string()).executes(it -> { - accessControl.grantAccess(it.getSource(), it.getArgument("name", String.class), it.getArgument("ip", String.class)); - return 0; - })))) - .then(LiteralArgumentBuilder.literal("list") - .executes(it -> { - accessControl.listAccesses(it.getSource()); - return 0; - }) - ).then(LiteralArgumentBuilder.literal("revoke") - .then(RequiredArgumentBuilder.argument("name", StringArgumentType.string()) - .executes(it -> { - accessControl.revokeAccess(it.getSource(), it.getArgument("name", String.class)); - return 0; - })) + LiteralArgumentBuilder.literal("addwl") + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_ADDWL.hasPermission(it.getPlayer())) + .then( + RequiredArgumentBuilder.argument("player", StringArgumentType.string()) + .executes(it -> handler.addPlayerOffline(it.getSource(), it.getArgument("player", String.class))) ) ); + dispatcher.register(LiteralArgumentBuilder.literal("back") + .requires(it -> it.getPlayer() != null) + .requires(it -> it.getPlayer().getLastDeathPos().isPresent()) + .requires(it -> SFConsts.COMMAND_BACK.hasPermission(it.getPlayer())) + .executes(handler::onBack)); + dispatcher.register(LiteralArgumentBuilder.literal("unblockserver") + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_UNBLOCKSERVER.hasPermission(it.getPlayer())) + .executes(handler::unblockServer)); + dispatcher.register(LiteralArgumentBuilder.literal("sudo") + .requires(it -> it.hasPermissionLevel(2) && it.getPlayer() != null) + .executes(handler::enterSudoMode) + ); + dispatcher.register(LiteralArgumentBuilder.literal("listperm") + .requires(it->it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTPERM.hasPermission(it.getPlayer())) + .executes(handler::listPerms) + ); + dispatcher.register(LiteralArgumentBuilder.literal("listoffline") + .requires(it->it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTOFFLINE.hasPermission(it.getPlayer())) + .executes(handler::listOffline) + ); } } diff --git a/src/main/java/io/ib67/sfcraft/access/AccessController.java b/src/main/java/io/ib67/sfcraft/access/AccessController.java deleted file mode 100644 index 7969d93..0000000 --- a/src/main/java/io/ib67/sfcraft/access/AccessController.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.ib67.sfcraft.access; - -import com.mojang.authlib.GameProfile; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.Collection; -import java.util.List; - -public interface AccessController { - boolean checkAccess(String name, String address); - - void grantAccess(String name, String condition); - - void revokeAccess(String name); - - Collection getEntries(); -} diff --git a/src/main/java/io/ib67/sfcraft/access/GrantedEntry.java b/src/main/java/io/ib67/sfcraft/access/GrantedEntry.java deleted file mode 100644 index c632f9c..0000000 --- a/src/main/java/io/ib67/sfcraft/access/GrantedEntry.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.ib67.sfcraft.access; - -import com.mojang.authlib.GameProfile; - -import java.net.InetSocketAddress; - -public record GrantedEntry(String name, String condition) { -} diff --git a/src/main/java/io/ib67/sfcraft/access/impl/SimpleAccessController.java b/src/main/java/io/ib67/sfcraft/access/impl/SimpleAccessController.java deleted file mode 100644 index afeb233..0000000 --- a/src/main/java/io/ib67/sfcraft/access/impl/SimpleAccessController.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.ib67.sfcraft.access.impl; - -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; -import io.ib67.sfcraft.access.AccessController; -import io.ib67.sfcraft.access.GrantedEntry; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.ArrayList; -import java.util.stream.Collectors; - -public class SimpleAccessController implements AccessController { - private final Multimap whitelist = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); - - public SimpleAccessController(List entries) { - entries.forEach(i -> whitelist.put(i.name(), i.condition())); - } - - @Override - public boolean checkAccess(String profile, String address) { - return whitelist.containsEntry(profile, address) || whitelist.containsEntry(profile, null); - } - - @Override - public void grantAccess(String profile, String condition) { - whitelist.put(profile, condition); - } - - @Override - public void revokeAccess(String profile) { - whitelist.removeAll(profile); - } - - @Override - public Collection getEntries() { - return whitelist.asMap().entrySet() - .stream().flatMap(it -> it.getValue().stream().map(i -> new GrantedEntry(it.getKey(), i))) - .collect(Collectors.toSet()); - } -} diff --git a/src/main/java/io/ib67/sfcraft/config/SFConfig.java b/src/main/java/io/ib67/sfcraft/config/SFConfig.java new file mode 100644 index 0000000..4f68d80 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/config/SFConfig.java @@ -0,0 +1,19 @@ +package io.ib67.sfcraft.config; + +import java.time.Clock; +import java.time.ZonedDateTime; +import java.util.List; + +public class SFConfig { + public int maintainceStartHour = 13; + public int maintainceEndHour = 17; + public boolean disableTemporarily; + + public boolean isClosed(Clock clock) { + if (disableTemporarily) return false; + var hourOfDay = ZonedDateTime.now(clock).getHour(); + var startHr = maintainceStartHour; + var endHr = maintainceEndHour; + return startHr < hourOfDay && hourOfDay < endHr; + } +} diff --git a/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java b/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java index 1c007de..2c68776 100644 --- a/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java +++ b/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java @@ -1,5 +1,6 @@ package io.ib67.sfcraft.geoip; +import com.maxmind.db.CHMCache; import com.maxmind.geoip2.DatabaseReader; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.record.Country; @@ -26,8 +27,8 @@ import static java.net.http.HttpResponse.BodyHandlers.ofFile; @Log4j2 public class GeoIPService implements Closeable { - private static final URI GEOIP_DOWNLOAD_URL = URI.create("https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb"); - private static final Path COUNTRY_MMDB_PATH = SFCraft.getInstance().getRoot().resolve("country.mmdb"); + private static final URI GEOIP_DOWNLOAD_URL = URI.create("http://sfclub.cc/GeoLite2-City.mmdb"); + private static final Path COUNTRY_MMDB_PATH = SFCraft.getInstance().getRoot().resolve("GeoLite2-City.mmdb"); private final DatabaseReader databaseReader; public GeoIPService() throws InterruptedException { @@ -36,12 +37,12 @@ public class GeoIPService implements Closeable { @SneakyThrows public Clock clockOf(InetAddress address) throws GeoIp2Exception { - return Clock.system(TimeZone.getTimeZone(databaseReader.country(address).getCountry().getIsoCode()).toZoneId()); + return Clock.system(TimeZone.getTimeZone(databaseReader.city(address).getLocation().getTimeZone()).toZoneId()); } private DatabaseReader loadData() throws InterruptedException { try { - return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).build(); + return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).withCache(new CHMCache()).build(); } catch (IOException ignored) { return updateData(); } diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java new file mode 100644 index 0000000..78a6d2a --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java @@ -0,0 +1,42 @@ +package io.ib67.sfcraft.mixin.server; + +import io.ib67.sfcraft.Helper; +import io.netty.channel.ChannelHandlerContext; +import net.minecraft.network.QueryableServer; +import net.minecraft.network.handler.LegacyQueryHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.Locale; + +@Mixin(LegacyQueryHandler.class) +public class LegacyQueryHandlerMixin { + SocketAddress address; + + @Inject(method = "channelRead", at = @At("HEAD")) + public void onChannelRead(ChannelHandlerContext ctx, Object msg, CallbackInfo ci) { + address = ctx.channel().remoteAddress(); + } + + @Redirect(method = "channelRead", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/handler/LegacyQueryHandler;getResponse(Lnet/minecraft/network/QueryableServer;)Ljava/lang/String;")) + private String getResponse(QueryableServer server) { + var version = server.getVersion(); + if (address instanceof InetSocketAddress address) { + version = Helper.getVersionString(address); + } + return String.format( + Locale.ROOT, + "§1\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d", + 127, + version, + server.getServerMotd(), + server.getCurrentPlayerCount(), + server.getMaxPlayerCount() + ); + } +} diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java new file mode 100644 index 0000000..e9d336f --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java @@ -0,0 +1,18 @@ +package io.ib67.sfcraft.mixin.server; + +import io.ib67.sfcraft.SFCraft; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayerEntity.class) +public class PlayerEntityMixin { + @Inject(method = "onDeath", at = @At("TAIL")) + public void onDeath(DamageSource damageSource, CallbackInfo ci) { + SFCraft.getInstance().getListener().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource); + } +} diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java new file mode 100644 index 0000000..d8b41b3 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java @@ -0,0 +1,24 @@ +package io.ib67.sfcraft.mixin.server; + +import io.ib67.sfcraft.SFCraft; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerMetadata; +import net.minecraft.server.network.ServerHandshakeNetworkHandler; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ServerHandshakeNetworkHandler.class) +public abstract class ServerHandshakeNetworkHandlerMixin { + @Shadow + @Final + private ClientConnection connection; + + @Redirect(method = "onHandshake", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getServerMetadata()Lnet/minecraft/server/ServerMetadata;")) + public ServerMetadata getMetadata(MinecraftServer instance) { + return SFCraft.getInstance().getListener().onMotdPing(instance,connection); + } +} diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java index 3023768..0b22457 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java @@ -26,7 +26,7 @@ public abstract class ServerLoginNetworkHandlerMixin { @Redirect(method = "onHello", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/ClientConnection;isLocal()Z")) public boolean isLocal(ClientConnection connection) { - return SFCraft.getListener().onPlayerLogin(currentPlayer, connection, this::disconnect); + return SFCraft.getInstance().getListener().onPlayerLogin(currentPlayer, connection, this::disconnect); } diff --git a/src/main/resources/sfcraft.mixins.json b/src/main/resources/sfcraft.mixins.json index d01702e..bb2b5fd 100644 --- a/src/main/resources/sfcraft.mixins.json +++ b/src/main/resources/sfcraft.mixins.json @@ -8,6 +8,9 @@ "client": [ ], "server": [ + "server.ServerHandshakeNetworkHandlerMixin", + "server.LegacyQueryHandlerMixin", + "server.PlayerEntityMixin", "server.ServerLoginNetworkHandlerMixin" ], "injectors": {