refactor WIP

This commit is contained in:
iceBear67 committed 2024-07-12 20:59:54 +08:00
1 parent b7e71962ed
commit 1df034a8ca
51 files changed
+1189 -544

No files matched your search

+3 -3
View File
@@ -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 不关注的信息。
一个简单的例子:
+1
View File
@@ -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)
-113
View File
@@ -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<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> 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;
}
}
@@ -0,0 +1,11 @@
package io.ib67.sfcraft;
public interface Lifecycle {
void onInitialize();
void onStateChange(State state);
enum State {
ENABLED, DISABLED,ERROR;
}
}
-150
View File
@@ -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<Text> 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<String>();
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<String> 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<PlayerEntity.SleepFailureReason, Unit> 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);
}
}
+24 -125
View File
@@ -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<ServerCommandSource> dispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
var handler = new Commands();
// 一般非玩家实体都有 >= 2 的 PermissionLevel,因此不需要额外检查。
dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("addwl")
.requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_ADDWL.hasPermission(it.getPlayer()))
.then(
RequiredArgumentBuilder.<ServerCommandSource, String>argument("player", StringArgumentType.string())
.executes(it -> handler.addPlayerOffline(it.getSource(), it.getArgument("player", String.class)))
)
);
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>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.<ServerCommandSource>literal("unblockserver")
.requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_UNBLOCKSERVER.hasPermission(it.getPlayer()))
.executes(handler::unblockServer));
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>literal("sudo")
.requires(it -> it.hasPermissionLevel(2) && it.getPlayer() != null)
.executes(handler::enterSudoMode)
);
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>literal("listperm")
.requires(it->it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTPERM.hasPermission(it.getPlayer()))
.executes(handler::listPerms)
);
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>literal("listoffline")
.requires(it->it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTOFFLINE.hasPermission(it.getPlayer()))
.executes(handler::listOffline)
);
}
}
@@ -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()
);
}
}
@@ -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();
}
}
@@ -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<PlayerPreLoginCallback> PRE_LOGIN = EventFactory.createArrayBacked(PlayerPreLoginCallback.class,
(listeners) -> (s, c, d) -> anyMatch(listeners, i -> i.onPlayerPreLogin(s, c, d)));
Event<PlayerSleepCallback> PLAYER_SLEEP = EventFactory.createArrayBacked(PlayerSleepCallback.class,
l -> (pl, pos) -> anyMatch(l, i -> i.onPlayerSleep(pl, pos), a -> a.left().isPresent())
.orElse(getEitherUnitR()));
Event<PlayerDeathCallback> PLAYER_DEATH = EventFactory.createArrayBacked(PlayerDeathCallback.class,
l -> (pl, damageSource) -> forEach(l, i -> i.onPlayerDeath(pl, damageSource)));
Event<ServerMotdCallback> 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<Text> disconnect);
}
interface PlayerSleepCallback {
Either<PlayerEntity.SleepFailureReason, Unit> onPlayerSleep(PlayerEntity player, BlockPos pos);
}
}
@@ -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<?, Unit> EITHER_UNIT_R = Either.right(Unit.INSTANCE);
static <T> Either<T, Unit> getEitherUnitR() {
return (Either<T, Unit>) EITHER_UNIT_R;
}
static <T, R> R first(T[] ts, Function<T, R> action) {
if (ts.length == 0) return null;
return action.apply(ts[0]);
}
static <T> void forEach(T[] ts, Consumer<T> action) {
for (T t : ts) {
action.accept(t);
}
}
static <T> boolean anyMatch(T[] ts, Predicate<T> predicate) {
for (T handler : ts) {
if (predicate.test(handler)) {
return false;
}
}
return true;
}
static <T, A> Optional<A> anyMatch(T[] ts, Function<T, A> function, Predicate<A> failurePredicate) {
A r = null;
for (T handler : ts) {
r = function.apply(handler);
if (failurePredicate.test(r)) {
return Optional.of(r);
}
}
return Optional.empty();
}
}
@@ -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";
}
@@ -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;
@@ -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
}
}
@@ -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;
}
@@ -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) {
}
}
@@ -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<Class<? extends ServerModule>> 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<String, ServerModule>();
for (Class<? extends ServerModule> 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<? extends ServerModule> featureClass) {
binder().bind(featureClass).in(Singleton.class);
}
}
@@ -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."));
}
}
}
@@ -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();
}
@@ -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 {
}
@@ -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<MinecraftServer> {
@Override
@SuppressWarnings("removal")
public MinecraftServer get() {
return SFCraft.server;
}
}
@@ -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());
}
}
@@ -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;
@@ -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;
}
}
@@ -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<Either<PlayerEntity.SleepFailureReason, Unit>> 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);
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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<ServerCommandSource> dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("addwl")
.requires(i -> isEnabled())
.requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_ADDWL.hasPermission(it.getPlayer()))
.then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("player", StringArgumentType.string())
.executes(it -> this.addPlayerOffline(it.getSource(), it.getArgument("player", String.class))))
);
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>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<ServerCommandSource> 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<Text> textConsumer) {
return state;
}
}
@@ -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<String> 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<ServerCommandSource> dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>literal("unblockserver")
.requires(i -> isEnabled())
.requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_UNBLOCKSERVER.hasPermission(it.getPlayer()))
.executes(this::unblockServer));
}
public int unblockServer(CommandContext<ServerCommandSource> 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<Text> 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;
}
}
@@ -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<String>();
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);
}
}
@@ -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<PlayerEntity>();
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);
}
@@ -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 " (未知)";
}
}
}
}
@@ -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;
@@ -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<ServerCommandSource> dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>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<ServerCommandSource> 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 传送到他们那里。(即使在对方也死亡的状态下)"));
}
}
}
@@ -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<ServerCommandSource> dispatcher,
CommandRegistryAccess registryAccess,
CommandManager.RegistrationEnvironment env) {
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>literal("sudo")
.requires(it -> this.isEnabled())
.requires(it -> it.hasPermissionLevel(2) && it.getPlayer() != null)
.executes(this::enterSudoMode)
);
dispatcher.register(LiteralArgumentBuilder.<ServerCommandSource>literal("listperm")
.requires(it -> this.isEnabled())
.requires(it -> it.hasPermissionLevel(2) || SFConsts.COMMAND_LISTPERM.hasPermission(it.getPlayer()))
.executes(this::listPerms)
);
}
public int enterSudoMode(CommandContext<ServerCommandSource> 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<ServerCommandSource> 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;
}
}
@@ -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<? extends ServerModule> getModules();
Optional<? extends ServerModule> getModule(String name);
Lifecycle.State getCurrentState();
/**
* @param state nextState, cannot be current state.
* @return error features
*/
List<? extends ServerModule> switchGlobalState(Lifecycle.State state);
}
@@ -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<String, ServerModule> modules;
@Getter
private Lifecycle.State currentState;
@Override
public List<? extends ServerModule> 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<ServerModule>();
for (ServerModule value : modules.values()) {
value.onStateChange(state);
if (value.getState() == Lifecycle.State.ERROR) {
result.add(value);
}
}
currentState = state;
return result;
}
@Override
public Collection<? extends ServerModule> getModules() {
return new ArrayList<>(modules.values());
}
@Override
public Optional<? extends ServerModule> getModule(String name) {
requireNonNull(name, "name cannot be null");
return Optional.ofNullable(modules.get(name));
}
}
@@ -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();
@@ -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<PlayerEntity.SleepFailureReason, Unit> 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);
}
}
@@ -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 {
/**
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
@@ -0,0 +1,6 @@
package io.ib67.sfcraft.registry.event;
public class SFRandomEventRegistry extends SimpleRandomEventRegistry {
public SFRandomEventRegistry() {
}
}
@@ -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;
@@ -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<String> 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);
@@ -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;
@@ -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 {
+1 -1
View File
@@ -13,7 +13,7 @@
"environment": "server",
"entrypoints": {
"main": [
"io.ib67.sfcraft.SFCraft"
"io.ib67.sfcraft.SFCraftInitializer"
]
},
"mixins": [