diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8af3f83..82faa38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ SFCraft 是一个 Fabric Mod,他的所有内容都是放在一起的,下列 在此类定义的字段将会自动作为配置文件的一部分生成。你可以通过调用 `SFCraft.getInstance().getConfig()` 得到配置对象。 - `io.ib67.sfcraft.Listener` 是事件监听器。大多数回调/事件的处理代码放在这里。 - `io.ib67.sfcraft.Commands` 是处理命令逻辑的地方,而命令在 `SFCraft#registerCommands` 方法注册。 - - `io.ib67.sfcraft.SFConsts` 存放一些频繁使用的常量,例如权限。 + - `io.ib67.sfcraft.util.SFConsts` 存放一些频繁使用的常量,例如权限。 - `io.ib67.sfcraft.mixin.server` 下是一些 `Mixin` 类,接下来会讲。 最后,`io.ib67.sfcraft.SFCraft` 是整个 Mod 的入口,也是核心部分。你可以从 `SFCraft.getInstance()` 返回的对象得到 Mod 里的大多数组件。 @@ -152,10 +152,10 @@ public class DawnAfterLongNightEvent extends RandomEvent { ## 聊天修改 -类似 `.xyz`, `@xx` 的功能都位于 `message.feature` 包下,且在 `SFMessageDecorator` 的构造方法中被注册。 +类似 `.xyz`, `@xx` 的功能都位于 `message.feature` 包下,且在 `SimpleMessageDecorator` 的构造方法中被注册。 它们实际上是 Minecraft 提供的 `MessageDecorator` 接口的实现。 -通常,一条消息进入服务器后会轮流经过许多 decorator 的处理。因此为了避免 siblings 嵌套问题,`SimpleMessageDecorator` (也就是 `SFMessageDecorator` 的超类) 会自动将 decorator 返回的 Text 展开—— +通常,一条消息进入服务器后会轮流经过许多 decorator 的处理。因此为了避免 siblings 嵌套问题,`SimpleMessageDecorator` (也就是 `SimpleMessageDecorator` 的超类) 会自动将 decorator 返回的 Text 展开—— 具体的说,将返回的结果 copy,去除 siblings,然后将 siblings 逐个加入一个缓存队列中,下一个 Decorator 将会逐个处理队列中的 `Text`,且每次处理完后都会清理掉原本的缓存队列。因此,在没有特殊需求的情况下,请总是返回你 Decorator 不关注的信息。 一个简单的例子: diff --git a/build.gradle b/build.gradle index 0d2e233..2218b0b 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" shadow('com.maxmind.geoip2:geoip2:4.2.0') + shadow('com.google.inject:guice:7.0.0') } configurations.implementation.extendsFrom(configurations.shadow) diff --git a/src/main/java/io/ib67/sfcraft/Commands.java b/src/main/java/io/ib67/sfcraft/Commands.java deleted file mode 100644 index 9e5b4f1..0000000 --- a/src/main/java/io/ib67/sfcraft/Commands.java +++ /dev/null @@ -1,113 +0,0 @@ -package io.ib67.sfcraft; - -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 { - - public Commands() { - ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); - } - - private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { - serverPlayNetworkHandler.getPlayer().removeCommandTag(SFConsts.SPECIAL_SUDO); - } - - 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.getX(), _pos.getY(), _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/Lifecycle.java b/src/main/java/io/ib67/sfcraft/Lifecycle.java new file mode 100644 index 0000000..3c3aab9 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/Lifecycle.java @@ -0,0 +1,11 @@ +package io.ib67.sfcraft; + +public interface Lifecycle { + void onInitialize(); + + void onStateChange(State state); + + enum State { + ENABLED, DISABLED,ERROR; + } +} diff --git a/src/main/java/io/ib67/sfcraft/Listener.java b/src/main/java/io/ib67/sfcraft/Listener.java deleted file mode 100644 index 8693055..0000000 --- a/src/main/java/io/ib67/sfcraft/Listener.java +++ /dev/null @@ -1,150 +0,0 @@ -package io.ib67.sfcraft; - -import com.maxmind.geoip2.exception.GeoIp2Exception; -import com.mojang.datafixers.util.Either; -import io.ib67.sfcraft.event.longnight.LongNightEvent; -import lombok.SneakyThrows; -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.entity.player.PlayerEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.network.ClientConnection; -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.Colors; -import net.minecraft.util.Unit; -import net.minecraft.util.Uuids; -import net.minecraft.util.math.BlockPos; - -import java.net.InetSocketAddress; -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) { - this.core = core; - } - - @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) { - 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 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. 下午 " + SFCraft.getInstance().getConfig().maintainceStartHour + " ~ " + SFCraft.getInstance().getConfig().maintainceEndHour + " 为服务器维护时间(防沉迷),在这段时间内无法加入服务器。")); - 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; - } - } - - public Either onPlayerSleep(PlayerEntity player, BlockPos pos) { - if (LongNightEvent.isRunning()) { - player.sendMessage(Text.literal("你感到焦躁不安!").withColor(Colors.LIGHT_RED) - .append(Text.literal(" (永夜事件期间无法睡觉)").withColor(Colors.LIGHT_GRAY))); - return Either.left(PlayerEntity.SleepFailureReason.NOT_POSSIBLE_HERE); - } - return Either.right(Unit.INSTANCE); - } -} diff --git a/src/main/java/io/ib67/sfcraft/SFCraft.java b/src/main/java/io/ib67/sfcraft/SFCraft.java index 774b58b..830a4de 100644 --- a/src/main/java/io/ib67/sfcraft/SFCraft.java +++ b/src/main/java/io/ib67/sfcraft/SFCraft.java @@ -1,109 +1,43 @@ package io.ib67.sfcraft; import com.google.gson.Gson; -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.google.inject.*; import io.ib67.sfcraft.config.SFConfig; -import io.ib67.sfcraft.geoip.GeoIPService; -import io.ib67.sfcraft.message.SFMessageDecorator; -import io.ib67.sfcraft.registry.event.RandomEventRegistry; -import io.ib67.sfcraft.event.SFRandomEventRegistry; +import io.ib67.sfcraft.inject.ConfigRoot; +import lombok.AccessLevel; 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.network.message.MessageDecorator; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.command.CommandManager; -import net.minecraft.server.command.ServerCommandSource; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Supplier; -public class SFCraft implements ModInitializer { - @Getter - private SFConfig config; - @Getter - private Listener listener; - @Getter - private static SFCraft instance; - @Getter - private GeoIPService geoIPService; - @Getter - private Path root; - @Getter - private MinecraftServer server; - @Getter - private String motd; - @Getter - private String updateLog; - @Getter - private RandomEventRegistry randomEventRegistry; - @Getter - private MessageDecorator messageDecorator; +public class SFCraft extends AbstractModule { + @Getter(AccessLevel.PUBLIC) + static Injector injector; + /** + * DO NOT USE THIS DIRECTLY + * USE {@link io.ib67.sfcraft.inject.MinecraftServerSupplier} IN DI INSTEAD. + */ + @Deprecated(forRemoval = true) + public static MinecraftServer server; - @Override + @Provides + @ConfigRoot + @Singleton + public static Path getRoot() { + return Path.of("sfcraft"); + } + + @Provides @SneakyThrows - public void onInitialize() { - instance = this; - root = Path.of("sfcraft"); + @Singleton + @Inject + private static SFConfig loadConfig(@ConfigRoot Path root, Gson gson) { if (Files.notExists(root)) { Files.createDirectory(root); } - listener = new Listener(this); - config = loadConfig(); - motd = loadMotd(); - updateLog = loadUpdateLog(); - geoIPService = new GeoIPService(); - - randomEventRegistry = setupRandomEvents(); - messageDecorator = setupMessageDecorator(); - - ServerPlayConnectionEvents.JOIN.register(listener::onPlayerJoin); - CommandRegistrationCallback.EVENT.register(this::registerCommands); - ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStarted); - - } - - private MessageDecorator setupMessageDecorator() { - return new SFMessageDecorator(); - } - - private SFRandomEventRegistry setupRandomEvents() { - return new SFRandomEventRegistry(); - } - - private void onServerStarted(MinecraftServer minecraftServer) { - server = minecraftServer; - server.setMotd(motd); - } - - @SneakyThrows - 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())); @@ -111,39 +45,4 @@ public class SFCraft implements ModInitializer { } return gson.fromJson(Files.readString(cfgFile), SFConfig.class); } - - private void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) { - var handler = new Commands(); - - // 一般非玩家实体都有 >= 2 的 PermissionLevel,因此不需要额外检查。 - dispatcher.register( - 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/SFCraftInitializer.java b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java new file mode 100644 index 0000000..3d335c4 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/SFCraftInitializer.java @@ -0,0 +1,64 @@ +package io.ib67.sfcraft; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.inject.*; +import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator; +import io.ib67.sfcraft.registry.event.SFRandomEventRegistry; +import io.ib67.sfcraft.init.GuiceModInitializer; +import io.ib67.sfcraft.geoip.GeoIPService; +import io.ib67.sfcraft.geoip.MaxMindGeoIPService; +import io.ib67.sfcraft.inject.MinecraftServerSupplier; +import io.ib67.sfcraft.module.MotdModule; +import io.ib67.sfcraft.module.OfflineExemptModule; +import io.ib67.sfcraft.module.PlayLimitModule; +import io.ib67.sfcraft.module.WelcomeModule; +import io.ib67.sfcraft.registry.event.RandomEventRegistry; +import io.ib67.sfcraft.module.command.BackModule; +import io.ib67.sfcraft.module.command.ManagementModule; +import lombok.extern.log4j.Log4j2; +import net.minecraft.server.MinecraftServer; + +@Log4j2 +public class SFCraftInitializer extends GuiceModInitializer { + @Override + protected void configure() { + super.configure(); + binder().bind(Gson.class).toInstance(new GsonBuilder().setPrettyPrinting().create()); + binder().bind(MinecraftServerSupplier.class).asEagerSingleton(); + registerServices(); + registerFeatures(); + } + + private void registerServices() { + binder().bind(GeoIPService.class).to(MaxMindGeoIPService.class); + binder().bind(RandomEventRegistry.class).to(SFRandomEventRegistry.class); + binder().bind(SimpleMessageDecorator.class).in(Singleton.class); + } + + private void registerFeatures() { + registerFeature(MotdModule.class); + registerFeature(WelcomeModule.class); + registerFeature(OfflineExemptModule.class); + registerFeature(PlayLimitModule.class); + registerFeature(BackModule.class); + registerFeature(ManagementModule.class); + } + + @Override + @SuppressWarnings("removal") + protected void onReady(MinecraftServer minecraftServer) { + log.info("SFCraft is loading"); + SFCraft.server = minecraftServer; + SFCraft.injector = getInjector(); + getModuleManager().switchGlobalState(Lifecycle.State.ENABLED); + } + + @Override + protected Injector onInit() { + return Guice.createInjector( + this, + new SFCraft() + ); + } +} diff --git a/src/main/java/io/ib67/sfcraft/ServerModule.java b/src/main/java/io/ib67/sfcraft/ServerModule.java new file mode 100644 index 0000000..3005554 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/ServerModule.java @@ -0,0 +1,41 @@ +package io.ib67.sfcraft; + +import com.google.inject.AbstractModule; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +@Getter +@Log4j2 +public abstract class ServerModule extends AbstractModule implements Lifecycle { + private State state; + + @Override + public final void onStateChange(State state) { + try { + switch (state) { + case ENABLED -> onEnable(); + case DISABLED -> onDisable(); + } + this.state = state; + } catch (Exception exception) { + log.error("An error occurred when " + getName() + " is changing its state into " + state); + this.state = State.ERROR; + } + } + + public void onDisable() { + + } + + public void onEnable() { + + } + + public final boolean isEnabled() { + return state == State.ENABLED; + } + + public String getName() { + return this.getClass().getSimpleName(); + } +} diff --git a/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java b/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java new file mode 100644 index 0000000..cc32c40 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/callback/SFCallbacks.java @@ -0,0 +1,51 @@ +package io.ib67.sfcraft.callback; + +import com.mojang.datafixers.util.Either; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerMetadata; +import net.minecraft.text.Text; +import net.minecraft.util.Unit; +import net.minecraft.util.math.BlockPos; + +import java.util.function.Consumer; + +import static io.ib67.sfcraft.callback.Utility.*; + +public interface SFCallbacks { + + Event PRE_LOGIN = EventFactory.createArrayBacked(PlayerPreLoginCallback.class, + (listeners) -> (s, c, d) -> anyMatch(listeners, i -> i.onPlayerPreLogin(s, c, d))); + Event PLAYER_SLEEP = EventFactory.createArrayBacked(PlayerSleepCallback.class, + l -> (pl, pos) -> anyMatch(l, i -> i.onPlayerSleep(pl, pos), a -> a.left().isPresent()) + .orElse(getEitherUnitR())); + + Event PLAYER_DEATH = EventFactory.createArrayBacked(PlayerDeathCallback.class, + l -> (pl, damageSource) -> forEach(l, i -> i.onPlayerDeath(pl, damageSource))); + Event MOTD = EventFactory.createArrayBacked(ServerMotdCallback.class, + l -> (s, c) -> first(l, t -> t.onMotd(s, c))); + + interface ServerMotdCallback { + ServerMetadata onMotd(MinecraftServer server, ClientConnection connection); + } + + interface PlayerDeathCallback { + void onPlayerDeath(PlayerEntity player, DamageSource damageSource); + } + + interface PlayerPreLoginCallback { + /** + * @return if let in + */ + boolean onPlayerPreLogin(String currentPlayer, ClientConnection connection, Consumer disconnect); + } + + interface PlayerSleepCallback { + + Either onPlayerSleep(PlayerEntity player, BlockPos pos); + } +} diff --git a/src/main/java/io/ib67/sfcraft/callback/Utility.java b/src/main/java/io/ib67/sfcraft/callback/Utility.java new file mode 100644 index 0000000..62616b9 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/callback/Utility.java @@ -0,0 +1,48 @@ +package io.ib67.sfcraft.callback; + +import com.mojang.datafixers.util.Either; +import net.minecraft.util.Unit; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +class Utility { + private static final Either EITHER_UNIT_R = Either.right(Unit.INSTANCE); + + static Either getEitherUnitR() { + return (Either) EITHER_UNIT_R; + } + + static R first(T[] ts, Function action) { + if (ts.length == 0) return null; + return action.apply(ts[0]); + } + + static void forEach(T[] ts, Consumer action) { + for (T t : ts) { + action.accept(t); + } + } + + static boolean anyMatch(T[] ts, Predicate predicate) { + for (T handler : ts) { + if (predicate.test(handler)) { + return false; + } + } + return true; + } + + static Optional anyMatch(T[] ts, Function function, Predicate failurePredicate) { + A r = null; + for (T handler : ts) { + r = function.apply(handler); + if (failurePredicate.test(r)) { + return Optional.of(r); + } + } + return Optional.empty(); + } +} \ No newline at end of file diff --git a/src/main/java/io/ib67/sfcraft/config/ConfigResources.java b/src/main/java/io/ib67/sfcraft/config/ConfigResources.java new file mode 100644 index 0000000..f021212 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/config/ConfigResources.java @@ -0,0 +1,6 @@ +package io.ib67.sfcraft.config; + +public class ConfigResources { + public static final String UPDATE_LOG="update.log"; + public static final String MOTD="motd.txt"; +} diff --git a/src/main/java/io/ib67/sfcraft/config/SFConfig.java b/src/main/java/io/ib67/sfcraft/config/SFConfig.java index 4f68d80..3d7cf3d 100644 --- a/src/main/java/io/ib67/sfcraft/config/SFConfig.java +++ b/src/main/java/io/ib67/sfcraft/config/SFConfig.java @@ -5,6 +5,7 @@ import java.time.ZonedDateTime; import java.util.List; public class SFConfig { + public boolean enableOfflineExempt = true; public int maintainceStartHour = 13; public int maintainceEndHour = 17; public boolean disableTemporarily; diff --git a/src/main/java/io/ib67/sfcraft/event/SFRandomEventRegistry.java b/src/main/java/io/ib67/sfcraft/event/SFRandomEventRegistry.java deleted file mode 100644 index 72d695a..0000000 --- a/src/main/java/io/ib67/sfcraft/event/SFRandomEventRegistry.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.ib67.sfcraft.event; - -import io.ib67.sfcraft.event.longnight.DawnAfterLongNightEvent; -import io.ib67.sfcraft.event.longnight.LongNightEvent; -import io.ib67.sfcraft.registry.event.SimpleRandomEventRegistry; -import net.minecraft.world.World; - -public class SFRandomEventRegistry extends SimpleRandomEventRegistry { - public SFRandomEventRegistry() { - registerEvent(LongNightEvent::new, World.OVERWORLD - , world -> !LongNightEvent.isRunning() - && world.getTimeOfDay() % 24000 == 18000 - && world.getRandom().nextBetween(0,100) < 5); // At midnight - registerEvent(DawnAfterLongNightEvent::new, World.OVERWORLD - , world -> world.getTimeOfDay() % 22200 == 0); // At dawn - } -} diff --git a/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java b/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java index 2c68776..6b3e726 100644 --- a/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java +++ b/src/main/java/io/ib67/sfcraft/geoip/GeoIPService.java @@ -1,76 +1,12 @@ 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; -import io.ib67.sfcraft.SFCraft; import lombok.SneakyThrows; -import lombok.extern.log4j.Log4j2; import java.io.Closeable; -import java.io.File; -import java.io.IOException; import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.nio.file.Files; -import java.nio.file.Path; import java.time.Clock; -import java.time.ZoneId; -import java.util.TimeZone; -import static java.net.http.HttpResponse.BodyHandlers.ofFile; - -@Log4j2 -public class GeoIPService implements Closeable { - 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 { - databaseReader = loadData(); - } - - @SneakyThrows - public Clock clockOf(InetAddress address) throws GeoIp2Exception { - 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()).withCache(new CHMCache()).build(); - } catch (IOException ignored) { - return updateData(); - } - } - - private DatabaseReader updateData() throws InterruptedException { - var client = HttpClient.newHttpClient(); - var req = HttpRequest.newBuilder(GEOIP_DOWNLOAD_URL).GET().build(); - int retries = 0; - do { - try { - log.info("Downloading GeoIP database, please wait...."); - client.send(req, ofFile(COUNTRY_MMDB_PATH)); - return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).build(); - } catch (IOException | InterruptedException e) { - log.error("Failed to download or the database is corrupted!", e); - int sleep = (int) Math.pow(2, ++retries); - log.info("Retrying after " + sleep + "sec."); - Thread.sleep(sleep); - } finally { - client.close(); - } - } while (retries < 4); - throw new InterruptedException("Cannot load GeoIP data."); - } - - @Override - public void close() throws IOException { - databaseReader.close(); - } +public interface GeoIPService extends Closeable { + Clock clockOf(InetAddress address) throws GeoIp2Exception; } diff --git a/src/main/java/io/ib67/sfcraft/geoip/MaxMindGeoIPService.java b/src/main/java/io/ib67/sfcraft/geoip/MaxMindGeoIPService.java new file mode 100644 index 0000000..80433c6 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/geoip/MaxMindGeoIPService.java @@ -0,0 +1,85 @@ +package io.ib67.sfcraft.geoip; + +import com.google.inject.Binder; +import com.google.inject.Inject; +import com.google.inject.Module; +import com.google.inject.Singleton; +import com.maxmind.db.CHMCache; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.inject.ConfigRoot; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.nio.file.Path; +import java.time.Clock; +import java.util.TimeZone; + +import static java.net.http.HttpResponse.BodyHandlers.ofFile; + +@Log4j2 +@Singleton +public class MaxMindGeoIPService implements Module, GeoIPService { + private static final URI GEOIP_DOWNLOAD_URL = URI.create("http://sfclub.cc/GeoLite2-City.mmdb"); + @Inject + @ConfigRoot + private Path configRoot; + private Path mmdbPath; + private final DatabaseReader databaseReader; + + public MaxMindGeoIPService() throws InterruptedException { + mmdbPath = configRoot.resolve("GeoLite2-City.mmdb"); + databaseReader = loadData(); + } + + @Override + @SneakyThrows + public Clock clockOf(InetAddress address) throws GeoIp2Exception { + return Clock.system(TimeZone.getTimeZone(databaseReader.city(address).getLocation().getTimeZone()).toZoneId()); + } + + private DatabaseReader loadData() throws InterruptedException { + try { + return new DatabaseReader.Builder(mmdbPath.toFile()).withCache(new CHMCache()).build(); + } catch (IOException ignored) { + return updateData(); + } + } + + private DatabaseReader updateData() throws InterruptedException { + var client = HttpClient.newHttpClient(); + var req = HttpRequest.newBuilder(GEOIP_DOWNLOAD_URL).GET().build(); + int retries = 0; + do { + try { + log.info("Downloading GeoIP database, please wait...."); + client.send(req, ofFile(mmdbPath)); + return new DatabaseReader.Builder(mmdbPath.toFile()).build(); + } catch (IOException | InterruptedException e) { + log.error("Failed to download or the database is corrupted!", e); + int sleep = (int) Math.pow(2, ++retries); + log.info("Retrying after " + sleep + "sec."); + Thread.sleep(sleep); + } finally { + client.close(); + } + } while (retries < 4); + throw new InterruptedException("Cannot load GeoIP data."); + } + + @Override + public void close() throws IOException { + databaseReader.close(); + } + + @Override + public void configure(Binder binder) { + + } +} diff --git a/src/main/java/io/ib67/sfcraft/init/GuiceModInitializer.java b/src/main/java/io/ib67/sfcraft/init/GuiceModInitializer.java new file mode 100644 index 0000000..5c3d31a --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/init/GuiceModInitializer.java @@ -0,0 +1,61 @@ +package io.ib67.sfcraft.init; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.module.manager.ModuleManager; +import io.ib67.sfcraft.module.manager.SimpleModuleManager; +import lombok.AccessLevel; +import lombok.Getter; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.MinecraftServer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public abstract class GuiceModInitializer extends AbstractModule implements ModInitializer { + public static AtomicBoolean initialized = new AtomicBoolean(false); + @Getter(AccessLevel.PROTECTED) + private Injector injector; + private ModuleManager moduleManager; + private final List> registeredFeatures = new ArrayList<>(); + + @Override + public void onInitialize() { + var handler = new PreInitHandler(initialized); + ServerPlayConnectionEvents.JOIN.register(handler::defend); + ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStarted); + injector = onInit(); + } + + private void onServerStarted(MinecraftServer server) { + var map = new HashMap(); + for (Class registeredFeature : registeredFeatures) { + var module = injector.getInstance(registeredFeature); + map.put(module.getName(), module); + } + moduleManager = new SimpleModuleManager(ImmutableMap.copyOf(map)); + onReady(server); + initialized.set(true); + } + + protected abstract Injector onInit(); + + protected abstract void onReady(MinecraftServer minecraftServer); + + @Provides + protected ModuleManager getModuleManager() { + return moduleManager; + } + + protected void registerFeature(Class featureClass) { + binder().bind(featureClass).in(Singleton.class); + } +} diff --git a/src/main/java/io/ib67/sfcraft/init/PreInitHandler.java b/src/main/java/io/ib67/sfcraft/init/PreInitHandler.java new file mode 100644 index 0000000..2bf1c2d --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/init/PreInitHandler.java @@ -0,0 +1,20 @@ +package io.ib67.sfcraft.init; + +import lombok.RequiredArgsConstructor; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.text.Text; + +import java.util.concurrent.atomic.AtomicBoolean; + +@RequiredArgsConstructor +public class PreInitHandler { + private final AtomicBoolean initialized; + + void defend(ServerPlayNetworkHandler handler, PacketSender sender, MinecraftServer server) { + if (!initialized.get()) { + handler.disconnect(Text.of("Server is initializing, please wait.")); + } + } +} diff --git a/src/main/java/io/ib67/sfcraft/inject/ConfigResource.java b/src/main/java/io/ib67/sfcraft/inject/ConfigResource.java new file mode 100644 index 0000000..5d83b45 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/inject/ConfigResource.java @@ -0,0 +1,12 @@ +package io.ib67.sfcraft.inject; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigResource { + String value(); +} diff --git a/src/main/java/io/ib67/sfcraft/inject/ConfigRoot.java b/src/main/java/io/ib67/sfcraft/inject/ConfigRoot.java new file mode 100644 index 0000000..f92310c --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/inject/ConfigRoot.java @@ -0,0 +1,14 @@ +package io.ib67.sfcraft.inject; + +import jakarta.inject.Qualifier; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Provides a {@link java.nio.file.Path} to config root (usually "./sfcraft") + */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigRoot { +} diff --git a/src/main/java/io/ib67/sfcraft/inject/MinecraftServerSupplier.java b/src/main/java/io/ib67/sfcraft/inject/MinecraftServerSupplier.java new file mode 100644 index 0000000..ebc9fde --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/inject/MinecraftServerSupplier.java @@ -0,0 +1,14 @@ +package io.ib67.sfcraft.inject; + +import io.ib67.sfcraft.SFCraft; +import net.minecraft.server.MinecraftServer; + +import java.util.function.Supplier; + +public class MinecraftServerSupplier implements Supplier { + @Override + @SuppressWarnings("removal") + public MinecraftServer get() { + return SFCraft.server; + } +} diff --git a/src/main/java/io/ib67/sfcraft/message/SFMessageDecorator.java b/src/main/java/io/ib67/sfcraft/message/SFMessageDecorator.java deleted file mode 100644 index 1824725..0000000 --- a/src/main/java/io/ib67/sfcraft/message/SFMessageDecorator.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.ib67.sfcraft.message; - -import io.ib67.sfcraft.message.feature.PingPlayerFeature; -import io.ib67.sfcraft.message.feature.SendLocationFeature; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.Text; - -public class SFMessageDecorator extends SimpleMessageDecorator { - public SFMessageDecorator() { - registerDecorator(new SendLocationFeature()); - registerDecorator(new PingPlayerFeature()); - } - -} diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java index 78a6d2a..d3a4f25 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/LegacyQueryHandlerMixin.java @@ -1,6 +1,6 @@ package io.ib67.sfcraft.mixin.server; -import io.ib67.sfcraft.Helper; +import io.ib67.sfcraft.util.Helper; import io.netty.channel.ChannelHandlerContext; import net.minecraft.network.QueryableServer; import net.minecraft.network.handler.LegacyQueryHandler; diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/MinecraftServerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/MinecraftServerMixin.java new file mode 100644 index 0000000..ef603e6 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/mixin/server/MinecraftServerMixin.java @@ -0,0 +1,18 @@ +package io.ib67.sfcraft.mixin.server; + +import io.ib67.sfcraft.module.randomevt.longnight.LongNightEvent; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + @ModifyConstant(method = "tickWorlds", constant = @Constant(intValue = 20)) + private int speedUpAtNight(int constant) { + if (LongNightEvent.isRunning()) { + return 5; + } + return constant; + } +} diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java index 1d5ec6b..5d71f64 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/PlayerEntityMixin.java @@ -1,7 +1,7 @@ package io.ib67.sfcraft.mixin.server; import com.mojang.datafixers.util.Either; -import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.callback.SFCallbacks; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.network.ServerPlayerEntity; @@ -17,13 +17,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; public class PlayerEntityMixin { @Inject(method = "onDeath", at = @At("TAIL")) public void onDeath(DamageSource damageSource, CallbackInfo ci) { - SFCraft.getInstance().getListener().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource); + SFCallbacks.PLAYER_DEATH.invoker().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource); } @Inject(method = "trySleep", at = @At("HEAD"), cancellable = true) public void onSleep(BlockPos pos, CallbackInfoReturnable> cir) { - var result = SFCraft.getInstance().getListener().onPlayerSleep((ServerPlayerEntity) (Object) this, pos); - if(result.left().isPresent()){ + var result = SFCallbacks.PLAYER_SLEEP.invoker().onPlayerSleep((ServerPlayerEntity) (Object) this, pos); + if (result.left().isPresent()) { cir.setReturnValue(result); } } diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java index d8b41b3..675fbf9 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerHandshakeNetworkHandlerMixin.java @@ -1,6 +1,7 @@ package io.ib67.sfcraft.mixin.server; import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.callback.SFCallbacks; import net.minecraft.network.ClientConnection; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ServerMetadata; @@ -19,6 +20,6 @@ public abstract class ServerHandshakeNetworkHandlerMixin { @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); + return SFCallbacks.MOTD.invoker().onMotd(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 0b22457..cc7a302 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerLoginNetworkHandlerMixin.java @@ -1,22 +1,35 @@ package io.ib67.sfcraft.mixin.server; -import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.callback.SFCallbacks; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerLoginNetworkHandler; import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Uuids; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; 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; + @Mixin(ServerLoginNetworkHandler.class) public abstract class ServerLoginNetworkHandlerMixin { @Shadow public abstract void disconnect(Text reason); + @Shadow + @Final + MinecraftServer server; + @Unique private String currentPlayer; @Inject(method = "onHello", at = @At("HEAD")) @@ -26,7 +39,24 @@ 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.getInstance().getListener().onPlayerLogin(currentPlayer, connection, this::disconnect); + if (!(connection.getAddress() instanceof InetSocketAddress)) { + return connection.isLocal(); // 碰上真 isLocal 了 + } + var offlineProfile = Uuids.getOfflinePlayerProfile(currentPlayer); + boolean _r; + if (server.getUserCache() == null) { + // falling back to whitelist + var wl = server.getPlayerManager().getWhitelist(); + _r = wl.isAllowed(offlineProfile); + } else { + var profile = server.getUserCache().getByUuid(offlineProfile.getId()); + _r = profile.isPresent(); + } + var result = SFCallbacks.PRE_LOGIN.invoker().onPlayerPreLogin(currentPlayer, connection, this::disconnect); + if (result) { + return _r; // (is offline -> !true -> startVerify) + } + return !_r; } diff --git a/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayNetworkHandlerMixin.java b/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayNetworkHandlerMixin.java index c3105ca..3bcad07 100644 --- a/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayNetworkHandlerMixin.java +++ b/src/main/java/io/ib67/sfcraft/mixin/server/ServerPlayNetworkHandlerMixin.java @@ -1,27 +1,21 @@ package io.ib67.sfcraft.mixin.server; import io.ib67.sfcraft.SFCraft; -import net.minecraft.network.message.MessageChainTaskQueue; +import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator; import net.minecraft.network.message.MessageDecorator; -import net.minecraft.network.message.SignedMessage; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayNetworkHandler; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.injection.*; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; @Mixin(ServerPlayNetworkHandler.class) public abstract class ServerPlayNetworkHandlerMixin { /** * @author icybear - * @reason small method, provide custom one + * @reason small method, overwrite to reduce overhead */ @Redirect(method = "method_44900", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getMessageDecorator()Lnet/minecraft/network/message/MessageDecorator;")) public MessageDecorator getMessageDecorator(MinecraftServer instance) { - return SFCraft.getInstance().getMessageDecorator(); + return SFCraft.getInjector().getInstance(SimpleMessageDecorator.class); } } diff --git a/src/main/java/io/ib67/sfcraft/module/MotdModule.java b/src/main/java/io/ib67/sfcraft/module/MotdModule.java new file mode 100644 index 0000000..508b004 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/MotdModule.java @@ -0,0 +1,49 @@ +package io.ib67.sfcraft.module; + +import com.google.inject.Inject; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import io.ib67.sfcraft.inject.MinecraftServerSupplier; +import io.ib67.sfcraft.util.Helper; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.config.ConfigResources; +import io.ib67.sfcraft.inject.ConfigResource; +import io.ib67.sfcraft.inject.ConfigRoot; +import net.minecraft.server.MinecraftServer; + +import java.nio.file.Path; + +public class MotdModule extends ServerModule { + @Inject + @ConfigRoot + private Path configRoot; + @Inject + private MinecraftServerSupplier minecraftServer; + protected String motd; + protected String originalMotd; + + @Override + public void onInitialize() { + motd = getMotd(configRoot); + } + + @Override + public void onEnable() { + originalMotd = minecraftServer.get().getServerMotd(); + minecraftServer.get().setMotd(motd); + } + + @Override + public void onDisable() { + minecraftServer.get().setMotd(originalMotd); + } + + @Provides + @ConfigResource(ConfigResources.MOTD) + @Inject + @Singleton + private static String getMotd(@ConfigRoot Path root) { + return Helper.getConfigResource(root, ConfigResources.MOTD).orElse("SaltedFish Club Server"); + } + +} diff --git a/src/main/java/io/ib67/sfcraft/module/OfflineExemptModule.java b/src/main/java/io/ib67/sfcraft/module/OfflineExemptModule.java new file mode 100644 index 0000000..05d4881 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/OfflineExemptModule.java @@ -0,0 +1,94 @@ +package io.ib67.sfcraft.module; + +import com.google.inject.Inject; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.ib67.sfcraft.util.SFConsts; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.callback.SFCallbacks; +import io.ib67.sfcraft.config.SFConfig; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.WhitelistEntry; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Uuids; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class OfflineExemptModule extends ServerModule { + @Inject + private SFConfig config; + private boolean state; + + @Override + public void onInitialize() { + state = config.enableOfflineExempt; + SFCallbacks.PRE_LOGIN.register(this::onPreLogin); + CommandRegistrationCallback.EVENT.register(this::registerCommand); + } + + private void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) { + dispatcher.register( + LiteralArgumentBuilder.literal("addwl") + .requires(i -> isEnabled()) + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_ADDWL.hasPermission(it.getPlayer())) + .then(RequiredArgumentBuilder.argument("player", StringArgumentType.string()) + .executes(it -> this.addPlayerOffline(it.getSource(), it.getArgument("player", String.class)))) + + ); + dispatcher.register(LiteralArgumentBuilder.literal("listoffline") + .requires(i -> isEnabled()) + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTOFFLINE.hasPermission(it.getPlayer())) + .executes(this::listOffline) + ); + } + + 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 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; + } + + @Override + public void onDisable() { + state = false; + } + + @Override + public void onEnable() { + state = true; + } + + private boolean onPreLogin(String s, ClientConnection connection, Consumer textConsumer) { + return state; + } +} diff --git a/src/main/java/io/ib67/sfcraft/module/PlayLimitModule.java b/src/main/java/io/ib67/sfcraft/module/PlayLimitModule.java new file mode 100644 index 0000000..bad0b16 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/PlayLimitModule.java @@ -0,0 +1,96 @@ +package io.ib67.sfcraft.module; + +import com.google.inject.Inject; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.ib67.sfcraft.util.Helper; +import io.ib67.sfcraft.util.SFConsts; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.callback.SFCallbacks; +import io.ib67.sfcraft.config.SFConfig; +import io.ib67.sfcraft.geoip.GeoIPService; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerMetadata; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +public class PlayLimitModule extends ServerModule { + private final Set motdRequestedIps = new HashSet<>(); + @Inject + GeoIPService geoIPService; + @Inject + SFConfig config; + + @Override + public void onInitialize() { + SFCallbacks.PRE_LOGIN.register(this::onPreLogin); + SFCallbacks.MOTD.register(this::onMotd); + CommandRegistrationCallback.EVENT.register(this::registerCommand); + } + + private void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) { + dispatcher.register(LiteralArgumentBuilder.literal("unblockserver") + .requires(i -> isEnabled()) + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_UNBLOCKSERVER.hasPermission(it.getPlayer())) + .executes(this::unblockServer)); + } + + public int unblockServer(CommandContext ctx) { + var state = config.disableTemporarily; + config.disableTemporarily = !state; + ctx.getSource().sendMessage(Text.of("Current state: " + !state)); + return 0; + } + + private ServerMetadata onMotd(MinecraftServer server, ClientConnection connection) { + var ip = connection.getAddressAsString(false); + var cached = motdRequestedIps.contains(ip); + if (!cached) { + motdRequestedIps.add(ip); + } + var originalMetadata = server.getServerMetadata(); + if (originalMetadata == null) return null; + String version = null; + if (connection.getAddress() instanceof InetSocketAddress address) { + version = Helper.getVersionString(address); + } + if (version != null && isEnabled()) { + return new ServerMetadata( + originalMetadata.description(), + originalMetadata.players(), + Optional.of(new ServerMetadata.Version(version, 0)), + cached ? Optional.empty() : originalMetadata.favicon(), + originalMetadata.secureChatEnforced() + ); + } else { + return originalMetadata; + } + + } + + private boolean onPreLogin(String currentPlayer, ClientConnection connection, Consumer disconnect) { + if (isEnabled() && connection.getAddress() instanceof InetSocketAddress address) { + try { + var clock = geoIPService.clockOf(address.getAddress()); + 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; + } +} diff --git a/src/main/java/io/ib67/sfcraft/module/WelcomeModule.java b/src/main/java/io/ib67/sfcraft/module/WelcomeModule.java new file mode 100644 index 0000000..5f3c47c --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/WelcomeModule.java @@ -0,0 +1,104 @@ +package io.ib67.sfcraft.module; + +import com.google.inject.Inject; +import com.google.inject.Provides; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.config.ConfigResources; +import io.ib67.sfcraft.config.SFConfig; +import io.ib67.sfcraft.geoip.GeoIPService; +import io.ib67.sfcraft.inject.ConfigResource; +import io.ib67.sfcraft.inject.ConfigRoot; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.WrittenBookContentComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.RawFilteredPair; +import net.minecraft.text.Text; + +import java.net.InetSocketAddress; +import java.nio.file.Path; +import java.time.Clock; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +import static io.ib67.sfcraft.util.Helper.getConfigResource; + +public class WelcomeModule extends ServerModule { + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + @Inject + private SFConfig config; + @Inject + @ConfigRoot + private Path root; + @Inject + private GeoIPService geoIPService; + private String updateLog; + + @Override + public void onInitialize() { + updateLog = getConfigResource(root, ConfigResources.UPDATE_LOG).orElse("他没写呢"); + ServerPlayConnectionEvents.JOIN.register(this::onPlayerJoin); + } + + @Provides + @ConfigResource(ConfigResources.UPDATE_LOG) + private String getUpdateLog() { + return updateLog; + } + + public void onPlayerJoin(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender + packetSender, MinecraftServer minecraftServer) { + var player = serverPlayNetworkHandler.getPlayer(); + if (!player.getCommandTags().contains("has_joined_before") && isEnabled()) { + 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. 下午 " + config.maintainceStartHour + " ~ " + config.maintainceEndHour + " 为服务器维护时间(防沉迷),在这段时间内无法加入服务器。")); + player.sendMessage(Text.of("如果您想邀请朋友加入服务器,可以向管理员提出申请。本服务器为正版服,但是离线玩家也可以加入(需备注)。")); + player.sendMessage(Text.of("为了账号安全起见,请尽量使用正版账号登录后加入。此外,正/盗同名将会被视为两个不同的玩家存档,数据不互通。")); + player.giveItemStack(generateHelperBook(updateLog, player)); + unlockRecipe(player); + } + } + + private ItemStack generateHelperBook(String updateLog, ServerPlayerEntity player) { + Clock clock = null; + if (player.networkHandler.getConnectionAddress() instanceof InetSocketAddress addr) { + try { + clock = geoIPService.clockOf(addr.getAddress()); + } catch (GeoIp2Exception ignored) { + } + } + if (clock == null) 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)); + 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); + } +} diff --git a/src/main/java/io/ib67/sfcraft/message/feature/PingPlayerFeature.java b/src/main/java/io/ib67/sfcraft/module/chat/ChatPingModule.java similarity index 73% rename from src/main/java/io/ib67/sfcraft/message/feature/PingPlayerFeature.java rename to src/main/java/io/ib67/sfcraft/module/chat/ChatPingModule.java index ab82e79..864073e 100644 --- a/src/main/java/io/ib67/sfcraft/message/feature/PingPlayerFeature.java +++ b/src/main/java/io/ib67/sfcraft/module/chat/ChatPingModule.java @@ -1,10 +1,12 @@ -package io.ib67.sfcraft.message.feature; +package io.ib67.sfcraft.module.chat; -import io.ib67.sfcraft.SFConsts; -import io.ib67.sfcraft.SFCraft; +import com.google.inject.Inject; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator; +import io.ib67.sfcraft.inject.MinecraftServerSupplier; +import io.ib67.sfcraft.util.SFConsts; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.network.message.MessageDecorator; -import net.minecraft.registry.RegistryKeys; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvent; @@ -13,18 +15,29 @@ import net.minecraft.text.Text; import net.minecraft.util.Colors; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.MatchResult; import java.util.regex.Pattern; -public class PingPlayerFeature implements MessageDecorator { +public class ChatPingModule extends ServerModule implements MessageDecorator { private static final Pattern PING = Pattern.compile("(@[\\w]+)?"); + @Inject + MinecraftServerSupplier serverSupplier; + @Inject + SimpleMessageDecorator messageDecorator; + + @Override + public void onInitialize() { + messageDecorator.registerDecorator(this); + } @Override public Text decorate(@Nullable ServerPlayerEntity sender, Text message) { + if (!isEnabled()) { + return message; + } if (sender != null) { if (!SFConsts.USE_AT.hasPermission(sender)) { return message; @@ -35,7 +48,7 @@ public class PingPlayerFeature implements MessageDecorator { var match = PING.matcher(text); if (!match.find()) return message; var foundPlayers = new HashSet(); - var names = List.of(SFCraft.getInstance().getServer().getPlayerNames()); + var names = List.of(serverSupplier.get().getPlayerNames()); var r = Text.literal(match.replaceAll(it -> this.matchPlayer(it, names, foundPlayers))); for (PlayerEntity foundPlayer : foundPlayers) { if (sender != null) { @@ -58,7 +71,7 @@ public class PingPlayerFeature implements MessageDecorator { final var d = r.substring(1); return playerNames.stream() .filter(it -> it.toLowerCase().startsWith(d)) - .peek(it -> foundPlayers.add(SFCraft.getInstance().getServer().getPlayerManager().getPlayer(it))) + .peek(it -> foundPlayers.add(serverSupplier.get().getPlayerManager().getPlayer(it))) .findFirst() .orElse(r); } diff --git a/src/main/java/io/ib67/sfcraft/message/feature/SendLocationFeature.java b/src/main/java/io/ib67/sfcraft/module/chat/ChatSendLocModule.java similarity index 71% rename from src/main/java/io/ib67/sfcraft/message/feature/SendLocationFeature.java rename to src/main/java/io/ib67/sfcraft/module/chat/ChatSendLocModule.java index 92a2412..9983ebc 100644 --- a/src/main/java/io/ib67/sfcraft/message/feature/SendLocationFeature.java +++ b/src/main/java/io/ib67/sfcraft/module/chat/ChatSendLocModule.java @@ -1,7 +1,10 @@ -package io.ib67.sfcraft.message.feature; +package io.ib67.sfcraft.module.chat; -import io.ib67.sfcraft.Helper; -import io.ib67.sfcraft.SFConsts; +import com.google.inject.Inject; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator; +import io.ib67.sfcraft.util.Helper; +import io.ib67.sfcraft.util.SFConsts; import net.minecraft.network.message.MessageDecorator; import net.minecraft.registry.RegistryKey; import net.minecraft.server.network.ServerPlayerEntity; @@ -10,15 +13,24 @@ import net.minecraft.text.Text; import net.minecraft.world.World; import org.jetbrains.annotations.Nullable; -public class SendLocationFeature implements MessageDecorator { +public class ChatSendLocModule extends ServerModule implements MessageDecorator { + @Inject + SimpleMessageDecorator messageDecorator; + + @Override + public void onInitialize() { + messageDecorator.registerDecorator(this); + } + @Override public Text decorate(@Nullable ServerPlayerEntity sender, Text message) { + if (!isEnabled()) return message; if (sender != null) { if (!SFConsts.USE_BROADCAST_LOCATION.hasPermission(sender)) { return message; } } - return ReplaceHelper.replace(sender, message, ".xyz", SendLocationFeature::generateLocText); + return ReplaceHelper.replace(sender, message, ".xyz", ChatSendLocModule::generateLocText); } private static Text generateLocText(ServerPlayerEntity sender) { @@ -45,4 +57,4 @@ public class SendLocationFeature implements MessageDecorator { return " (未知)"; } } -} +} \ No newline at end of file diff --git a/src/main/java/io/ib67/sfcraft/message/feature/ReplaceHelper.java b/src/main/java/io/ib67/sfcraft/module/chat/ReplaceHelper.java similarity index 95% rename from src/main/java/io/ib67/sfcraft/message/feature/ReplaceHelper.java rename to src/main/java/io/ib67/sfcraft/module/chat/ReplaceHelper.java index 533768d..dd9b010 100644 --- a/src/main/java/io/ib67/sfcraft/message/feature/ReplaceHelper.java +++ b/src/main/java/io/ib67/sfcraft/module/chat/ReplaceHelper.java @@ -1,4 +1,4 @@ -package io.ib67.sfcraft.message.feature; +package io.ib67.sfcraft.module.chat; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; diff --git a/src/main/java/io/ib67/sfcraft/module/command/BackModule.java b/src/main/java/io/ib67/sfcraft/module/command/BackModule.java new file mode 100644 index 0000000..541211f --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/command/BackModule.java @@ -0,0 +1,61 @@ +package io.ib67.sfcraft.module.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.ib67.sfcraft.util.Helper; +import io.ib67.sfcraft.util.SFConsts; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.callback.SFCallbacks; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; + +public class BackModule extends ServerModule { + @Override + public void onInitialize() { + SFCallbacks.PLAYER_DEATH.register(this::onPlayerDeath); + CommandRegistrationCallback.EVENT.register(this::registerCommands); + } + + private void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) { + dispatcher.register(LiteralArgumentBuilder.literal("back") + .requires(it -> this.isEnabled()) + .requires(it -> it.getPlayer() != null) + .requires(it -> it.getPlayer().getLastDeathPos().isPresent()) + .requires(it -> SFConsts.COMMAND_BACK.hasPermission(it.getPlayer())) + .executes(this::onBack)); + } + + 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.getX(), _pos.getY(), _pos.getZ(), 0, 0); + return 0; + } + player.sendMessage(Text.of("周围没有玩家。").copy().withColor(Colors.RED)); + } + return 0; + } + + public void onPlayerDeath(PlayerEntity player, DamageSource damageSource) { + if (this.isEnabled() && Helper.canBack((ServerPlayerEntity) player)) { + player.sendMessage(Text.of("Tip: 死亡地点附近有玩家,可以使用 /back 传送到他们那里。(即使在对方也死亡的状态下)")); + } + } +} diff --git a/src/main/java/io/ib67/sfcraft/module/command/ManagementModule.java b/src/main/java/io/ib67/sfcraft/module/command/ManagementModule.java new file mode 100644 index 0000000..957e1a5 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/command/ManagementModule.java @@ -0,0 +1,73 @@ +package io.ib67.sfcraft.module.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import io.ib67.sfcraft.util.Permission; +import io.ib67.sfcraft.util.SFConsts; +import io.ib67.sfcraft.ServerModule; +import lombok.SneakyThrows; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; + +import java.lang.reflect.Field; + +public class ManagementModule extends ServerModule { + @Override + public void onInitialize() { + CommandRegistrationCallback.EVENT.register(this::registerCommands); + ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); + } + + private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { + serverPlayNetworkHandler.getPlayer().removeCommandTag(SFConsts.SPECIAL_SUDO); + } + + private void registerCommands( + CommandDispatcher dispatcher, + CommandRegistryAccess registryAccess, + CommandManager.RegistrationEnvironment env) { + dispatcher.register(LiteralArgumentBuilder.literal("sudo") + .requires(it -> this.isEnabled()) + .requires(it -> it.hasPermissionLevel(2) && it.getPlayer() != null) + .executes(this::enterSudoMode) + ); + dispatcher.register(LiteralArgumentBuilder.literal("listperm") + .requires(it -> this.isEnabled()) + .requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTPERM.hasPermission(it.getPlayer())) + .executes(this::listPerms) + ); + } + + public int enterSudoMode(CommandContext ctx) { + var player = ctx.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 ctx) { + var src = ctx.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; + } +} diff --git a/src/main/java/io/ib67/sfcraft/module/manager/ModuleManager.java b/src/main/java/io/ib67/sfcraft/module/manager/ModuleManager.java new file mode 100644 index 0000000..c41125a --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/manager/ModuleManager.java @@ -0,0 +1,22 @@ +package io.ib67.sfcraft.module.manager; + +import io.ib67.sfcraft.Lifecycle; +import io.ib67.sfcraft.ServerModule; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface ModuleManager { + Collection getModules(); + + Optional getModule(String name); + + Lifecycle.State getCurrentState(); + + /** + * @param state nextState, cannot be current state. + * @return error features + */ + List switchGlobalState(Lifecycle.State state); +} diff --git a/src/main/java/io/ib67/sfcraft/module/manager/SimpleModuleManager.java b/src/main/java/io/ib67/sfcraft/module/manager/SimpleModuleManager.java new file mode 100644 index 0000000..2230178 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/manager/SimpleModuleManager.java @@ -0,0 +1,50 @@ +package io.ib67.sfcraft.module.manager; + +import io.ib67.sfcraft.Lifecycle; +import io.ib67.sfcraft.ServerModule; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static java.util.Objects.requireNonNull; + +@RequiredArgsConstructor +public class SimpleModuleManager implements ModuleManager { + private final Map modules; + @Getter + private Lifecycle.State currentState; + + @Override + public List switchGlobalState(Lifecycle.State state) { + requireNonNull(state, "state cannot be null"); + if (currentState == state) { + throw new IllegalArgumentException("currentState == targetState == " + state); + } + if (state == Lifecycle.State.ERROR) { + throw new IllegalArgumentException("targetState == " + state); + } + var result = new ArrayList(); + for (ServerModule value : modules.values()) { + value.onStateChange(state); + if (value.getState() == Lifecycle.State.ERROR) { + result.add(value); + } + } + currentState = state; + return result; + } + + @Override + public Collection getModules() { + return new ArrayList<>(modules.values()); + } + + @Override + public Optional getModule(String name) { + requireNonNull(name, "name cannot be null"); + return Optional.ofNullable(modules.get(name)); + } +} diff --git a/src/main/java/io/ib67/sfcraft/registry/event/ActiveEvent.java b/src/main/java/io/ib67/sfcraft/module/randomevt/ActiveEvent.java similarity index 52% rename from src/main/java/io/ib67/sfcraft/registry/event/ActiveEvent.java rename to src/main/java/io/ib67/sfcraft/module/randomevt/ActiveEvent.java index 77594f2..cb37a84 100644 --- a/src/main/java/io/ib67/sfcraft/registry/event/ActiveEvent.java +++ b/src/main/java/io/ib67/sfcraft/module/randomevt/ActiveEvent.java @@ -1,6 +1,4 @@ -package io.ib67.sfcraft.registry.event; - -import io.ib67.sfcraft.event.RandomEvent; +package io.ib67.sfcraft.module.randomevt; public interface ActiveEvent { RandomEvent getEvent(); diff --git a/src/main/java/io/ib67/sfcraft/module/randomevt/LongNightModule.java b/src/main/java/io/ib67/sfcraft/module/randomevt/LongNightModule.java new file mode 100644 index 0000000..0220899 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/module/randomevt/LongNightModule.java @@ -0,0 +1,42 @@ +package io.ib67.sfcraft.module.randomevt; + +import com.google.inject.Inject; +import com.mojang.datafixers.util.Either; +import io.ib67.sfcraft.ServerModule; +import io.ib67.sfcraft.callback.SFCallbacks; +import io.ib67.sfcraft.module.randomevt.longnight.DawnAfterLongNightEvent; +import io.ib67.sfcraft.module.randomevt.longnight.LongNightEvent; +import io.ib67.sfcraft.registry.event.RandomEventRegistry; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Unit; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public class LongNightModule extends ServerModule { + @Inject + RandomEventRegistry registry; + + @Override + public void onInitialize() { + registry.registerEvent(LongNightEvent::new, World.OVERWORLD + , world -> !LongNightEvent.isRunning() + && isEnabled() + && world.getTimeOfDay() % 24000 == 18000 + && world.getRandom().nextBetween(0, 100) < 5); // At midnight + registry.registerEvent(DawnAfterLongNightEvent::new, World.OVERWORLD + , world -> world.getTimeOfDay() % 22200 == 0); // At dawn + SFCallbacks.PLAYER_SLEEP.register(this::onPlayerSleep); + } + + public Either onPlayerSleep(PlayerEntity player, BlockPos pos) { + if (!isEnabled()) return Either.right(Unit.INSTANCE); + if (LongNightEvent.isRunning()) { + player.sendMessage(Text.literal("你感到焦躁不安!").withColor(Colors.LIGHT_RED) + .append(Text.literal(" (永夜事件期间无法睡觉)").withColor(Colors.LIGHT_GRAY))); + return Either.left(PlayerEntity.SleepFailureReason.NOT_POSSIBLE_HERE); + } + return Either.right(Unit.INSTANCE); + } +} diff --git a/src/main/java/io/ib67/sfcraft/event/RandomEvent.java b/src/main/java/io/ib67/sfcraft/module/randomevt/RandomEvent.java similarity index 69% rename from src/main/java/io/ib67/sfcraft/event/RandomEvent.java rename to src/main/java/io/ib67/sfcraft/module/randomevt/RandomEvent.java index 13bcf83..8e56279 100644 --- a/src/main/java/io/ib67/sfcraft/event/RandomEvent.java +++ b/src/main/java/io/ib67/sfcraft/module/randomevt/RandomEvent.java @@ -1,7 +1,4 @@ -package io.ib67.sfcraft.event; - -import net.minecraft.registry.RegistryKey; -import net.minecraft.world.World; +package io.ib67.sfcraft.module.randomevt; public abstract class RandomEvent { /** diff --git a/src/main/java/io/ib67/sfcraft/event/longnight/DawnAfterLongNightEvent.java b/src/main/java/io/ib67/sfcraft/module/randomevt/longnight/DawnAfterLongNightEvent.java similarity index 90% rename from src/main/java/io/ib67/sfcraft/event/longnight/DawnAfterLongNightEvent.java rename to src/main/java/io/ib67/sfcraft/module/randomevt/longnight/DawnAfterLongNightEvent.java index fa114d5..32abfc3 100644 --- a/src/main/java/io/ib67/sfcraft/event/longnight/DawnAfterLongNightEvent.java +++ b/src/main/java/io/ib67/sfcraft/module/randomevt/longnight/DawnAfterLongNightEvent.java @@ -1,6 +1,6 @@ -package io.ib67.sfcraft.event.longnight; +package io.ib67.sfcraft.module.randomevt.longnight; -import io.ib67.sfcraft.event.RandomEvent; +import io.ib67.sfcraft.module.randomevt.RandomEvent; import lombok.RequiredArgsConstructor; import net.minecraft.text.Text; import net.minecraft.util.Colors; diff --git a/src/main/java/io/ib67/sfcraft/event/longnight/LongNightEvent.java b/src/main/java/io/ib67/sfcraft/module/randomevt/longnight/LongNightEvent.java similarity index 93% rename from src/main/java/io/ib67/sfcraft/event/longnight/LongNightEvent.java rename to src/main/java/io/ib67/sfcraft/module/randomevt/longnight/LongNightEvent.java index 30cf451..22e5049 100644 --- a/src/main/java/io/ib67/sfcraft/event/longnight/LongNightEvent.java +++ b/src/main/java/io/ib67/sfcraft/module/randomevt/longnight/LongNightEvent.java @@ -1,6 +1,6 @@ -package io.ib67.sfcraft.event.longnight; +package io.ib67.sfcraft.module.randomevt.longnight; -import io.ib67.sfcraft.event.RandomEvent; +import io.ib67.sfcraft.module.randomevt.RandomEvent; import lombok.RequiredArgsConstructor; import net.minecraft.text.Text; import net.minecraft.util.Colors; diff --git a/src/main/java/io/ib67/sfcraft/message/SimpleMessageDecorator.java b/src/main/java/io/ib67/sfcraft/registry/chat/SimpleMessageDecorator.java similarity index 91% rename from src/main/java/io/ib67/sfcraft/message/SimpleMessageDecorator.java rename to src/main/java/io/ib67/sfcraft/registry/chat/SimpleMessageDecorator.java index d8240d5..be3ea8a 100644 --- a/src/main/java/io/ib67/sfcraft/message/SimpleMessageDecorator.java +++ b/src/main/java/io/ib67/sfcraft/registry/chat/SimpleMessageDecorator.java @@ -1,4 +1,4 @@ -package io.ib67.sfcraft.message; +package io.ib67.sfcraft.registry.chat; import net.minecraft.network.message.MessageDecorator; import net.minecraft.server.network.ServerPlayerEntity; @@ -15,7 +15,7 @@ public class SimpleMessageDecorator implements MessageDecorator { } - protected void registerDecorator(MessageDecorator decorator) { + public void registerDecorator(MessageDecorator decorator) { decorators.add(decorator); } diff --git a/src/main/java/io/ib67/sfcraft/registry/event/RandomEventRegistry.java b/src/main/java/io/ib67/sfcraft/registry/event/RandomEventRegistry.java index 205dccc..a6475a4 100644 --- a/src/main/java/io/ib67/sfcraft/registry/event/RandomEventRegistry.java +++ b/src/main/java/io/ib67/sfcraft/registry/event/RandomEventRegistry.java @@ -1,6 +1,7 @@ package io.ib67.sfcraft.registry.event; -import io.ib67.sfcraft.event.RandomEvent; +import io.ib67.sfcraft.module.randomevt.ActiveEvent; +import io.ib67.sfcraft.module.randomevt.RandomEvent; import net.minecraft.registry.RegistryKey; import net.minecraft.world.World; diff --git a/src/main/java/io/ib67/sfcraft/registry/event/SFRandomEventRegistry.java b/src/main/java/io/ib67/sfcraft/registry/event/SFRandomEventRegistry.java new file mode 100644 index 0000000..0e4fe34 --- /dev/null +++ b/src/main/java/io/ib67/sfcraft/registry/event/SFRandomEventRegistry.java @@ -0,0 +1,6 @@ +package io.ib67.sfcraft.registry.event; + +public class SFRandomEventRegistry extends SimpleRandomEventRegistry { + public SFRandomEventRegistry() { + } +} diff --git a/src/main/java/io/ib67/sfcraft/registry/event/SimpleRandomEventRegistry.java b/src/main/java/io/ib67/sfcraft/registry/event/SimpleRandomEventRegistry.java index 3ef24ae..909ceae 100644 --- a/src/main/java/io/ib67/sfcraft/registry/event/SimpleRandomEventRegistry.java +++ b/src/main/java/io/ib67/sfcraft/registry/event/SimpleRandomEventRegistry.java @@ -1,11 +1,11 @@ package io.ib67.sfcraft.registry.event; -import io.ib67.sfcraft.event.RandomEvent; +import io.ib67.sfcraft.module.randomevt.ActiveEvent; +import io.ib67.sfcraft.module.randomevt.RandomEvent; import lombok.Getter; import lombok.RequiredArgsConstructor; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.minecraft.registry.RegistryKey; -import net.minecraft.registry.RegistryKeys; import net.minecraft.server.world.ServerWorld; import net.minecraft.world.World; diff --git a/src/main/java/io/ib67/sfcraft/Helper.java b/src/main/java/io/ib67/sfcraft/util/Helper.java similarity index 60% rename from src/main/java/io/ib67/sfcraft/Helper.java rename to src/main/java/io/ib67/sfcraft/util/Helper.java index 1f0022c..5520e68 100644 --- a/src/main/java/io/ib67/sfcraft/Helper.java +++ b/src/main/java/io/ib67/sfcraft/util/Helper.java @@ -1,9 +1,16 @@ -package io.ib67.sfcraft; +package io.ib67.sfcraft.util; import com.maxmind.geoip2.exception.GeoIp2Exception; +import io.ib67.sfcraft.SFCraft; +import io.ib67.sfcraft.config.SFConfig; +import io.ib67.sfcraft.geoip.GeoIPService; +import lombok.SneakyThrows; import net.minecraft.server.network.ServerPlayerEntity; import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; public class Helper { public static boolean canBack(ServerPlayerEntity player) { @@ -17,9 +24,10 @@ public class Helper { } public static String getVersionString(InetSocketAddress address) { + var geoIp = SFCraft.getInjector().getInstance(GeoIPService.class); + var config = SFCraft.getInjector().getInstance(SFConfig.class); try { - var clock = SFCraft.getInstance().getGeoIPService().clockOf(address.getAddress()); - var config = SFCraft.getInstance().getConfig(); + var clock = geoIp.clockOf(address.getAddress()); if (config.isClosed(clock)) { return "防沉迷: " + config.maintainceStartHour + "~" + config.maintainceEndHour; } @@ -28,6 +36,13 @@ public class Helper { } return null; } + @SneakyThrows + public static Optional getConfigResource(Path root, String resource) { + if (Files.exists(root)) { + return Optional.of(Files.readString(root.resolve(resource))); + } + return Optional.empty(); + } public static int fromRgb(int r, int g, int b) { return ((r & 0x0ff) << 16) | ((g & 0x0ff) << 8) | (b & 0x0ff); diff --git a/src/main/java/io/ib67/sfcraft/Permission.java b/src/main/java/io/ib67/sfcraft/util/Permission.java similarity index 96% rename from src/main/java/io/ib67/sfcraft/Permission.java rename to src/main/java/io/ib67/sfcraft/util/Permission.java index 28bbfef..13520a7 100644 --- a/src/main/java/io/ib67/sfcraft/Permission.java +++ b/src/main/java/io/ib67/sfcraft/util/Permission.java @@ -1,6 +1,5 @@ -package io.ib67.sfcraft; +package io.ib67.sfcraft.util; -import lombok.Getter; import net.minecraft.entity.Entity; import java.util.Objects; diff --git a/src/main/java/io/ib67/sfcraft/SFConsts.java b/src/main/java/io/ib67/sfcraft/util/SFConsts.java similarity index 93% rename from src/main/java/io/ib67/sfcraft/SFConsts.java rename to src/main/java/io/ib67/sfcraft/util/SFConsts.java index beb2dd3..6147434 100644 --- a/src/main/java/io/ib67/sfcraft/SFConsts.java +++ b/src/main/java/io/ib67/sfcraft/util/SFConsts.java @@ -1,8 +1,8 @@ -package io.ib67.sfcraft; +package io.ib67.sfcraft.util; +import io.ib67.sfcraft.util.Permission; import lombok.experimental.UtilityClass; import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.server.network.ServerPlayerEntity; @UtilityClass public class SFConsts { diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 3456a94..1f5bd6c 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -13,7 +13,7 @@ "environment": "server", "entrypoints": { "main": [ - "io.ib67.sfcraft.SFCraft" + "io.ib67.sfcraft.SFCraftInitializer" ] }, "mixins": [