This commit is contained in:
iceBear67 committed 2024-07-09 18:13:03 +08:00
1 parent 16fd49be35
commit b1a611a3e6
20 files changed
+519 -168

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# SFCraft
本仓库为 S.F Craft 服务器的功能 Mod, S.F Craft 是一个邀请制的纯原版生存服务器。
本插件专门为 SFC 定制,因此您可能需要花费大多功夫来将其用在你的服务器上(比如修改大量的硬编码字符串,强制启用的功能,等等),因此,并不建议这么做。
本仓库开源供有能力的玩家贡献。
# Features
- 允许指定离线玩家进入正版服务器,与白名单兼容 (`/addwl`)
需要注意的是,如果使用此功能添加了正版玩家的名字,那么正版玩家将会以离线用户的 UUID 游玩,这会导致皮肤不显示等问题。
- 限制性的 `/back` 命令
切合服务器的需要,本服务器的 `/back` 命令仅在死亡地点半径 100 内有人的情况下才可传送,且传送到附近玩家的位置。
- 更新日志速览
新来的玩家将会发放小册子,简要概述服务器特性。
该书内容从 `sfcraft/update.log` 生成,每 128 字符一张纸。
- 防沉迷
保护每一个玩家的下午,支持自动识别时区,需要下载 GeoIP 数据库 (45MB)。
![img.png](assets/img.png)
- 人文关怀
对于一些年龄比较小的玩家还有一些不太熟悉 MC 的玩家,我们特地设计了一些赦免权,例如无限制的 `/back` 命令。
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+16 -1
View File
@@ -2,6 +2,7 @@ plugins {
id 'fabric-loom' version '1.6-SNAPSHOT'
id 'maven-publish'
id("io.freefair.lombok") version "8.6"
id("com.github.johnrengelman.shadow") version "8.1.1"
}
version = project.mod_version
@@ -28,7 +29,21 @@ dependencies {
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
implementation 'com.maxmind.geoip2:geoip2:4.2.0'
shadow('com.maxmind.geoip2:geoip2:4.2.0')
}
configurations.implementation.extendsFrom(configurations.shadow)
shadowJar {
configurations = [ project.configurations.shadow ]
exclude("META-INF")
}
remapJar {
// wait until the shadowJar is done
dependsOn(shadowJar)
mustRunAfter(shadowJar)
// Set the input jar for the task. Here use the shadow Jar that include the .class of the transitive dependency
inputFile = file(shadowJar.archivePath)
}
processResources {
+88 -21
View File
@@ -1,46 +1,113 @@
package io.ib67.sfcraft;
import io.ib67.sfcraft.access.AccessController;
import com.mojang.brigadier.context.CommandContext;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.WhitelistEntry;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.dedicated.MinecraftDedicatedServer;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.text.Text;
import net.minecraft.text.TextColor;
import net.minecraft.util.Colors;
import net.minecraft.util.StringHelper;
import net.minecraft.util.Uuids;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class Commands {
@RequiredArgsConstructor
static class AccessControl {
private final AccessController controller;
public void grantAccess(ServerCommandSource sender, String name, @Nullable String ip) {
if (!sender.hasPermissionLevel(2)) {
sender.sendMessage(Text.of("Permission denied."));
return;
public Commands() {
ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect);
}
if (!StringHelper.isValidPlayerName(name)) {
sender.sendMessage(Text.of("Invalid player name."));
return;
private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) {
serverPlayNetworkHandler.getPlayer().removeCommandTag(SFConsts.SPECIAL_SUDO);
}
if (ip == null) {
sender.sendMessage(Text.of("You're granting ANY access to player " + name + "!"));
controller.grantAccess(name, null);
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 {
controller.grantAccess(name, ip);
if (SFConsts.UNLIMITED_COMMAND_BACK.hasPermission(player)) {
player.teleport(wld, pos.pos().getX(), pos.pos().getY(), pos.pos().getZ(), 0, 0);
return 0;
}
sender.sendMessage(Text.of(name + ": Access granted."));
player.sendMessage(Text.of("周围没有玩家。").copy().withColor(Colors.RED));
}
return 0;
}
public void revokeAccess(ServerCommandSource sender, String name) {
controller.revokeAccess(name);
sender.sendMessage(Text.of(name + ": Permission revoked."));
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 void listAccesses(ServerCommandSource sender) {
controller.getEntries().forEach(it -> sender.sendMessage(Text.of(it.name() + ": " + (it.condition() == null ? "ANY" : it.condition()))));
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;
}
}
+31
View File
@@ -0,0 +1,31 @@
package io.ib67.sfcraft;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import net.minecraft.server.network.ServerPlayerEntity;
import java.net.InetSocketAddress;
public class Helper {
public static boolean canBack(ServerPlayerEntity player) {
if (!SFConsts.COMMAND_BACK.hasPermission(player)) return false;
var pos = player.getLastDeathPos().get();
var wld = player.server.getWorld(pos.dimension());
var _pos = pos.pos();
if (wld == null) return false;
var nearby = wld.getClosestPlayer(_pos.getX(), _pos.getY(), _pos.getZ(), 20, true);
return nearby != null;
}
public static String getVersionString(InetSocketAddress address) {
try {
var clock = SFCraft.getInstance().getGeoIPService().clockOf(address.getAddress());
var config = SFCraft.getInstance().getConfig();
if (config.isClosed(clock)) {
return "防沉迷: " + config.maintainceStartHour + "~" + config.maintainceEndHour;
}
} catch (GeoIp2Exception ignored) {
return "Not Available!";
}
return null;
}
}
+111 -14
View File
@@ -1,17 +1,32 @@
package io.ib67.sfcraft;
import com.maxmind.geoip2.record.City;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import lombok.SneakyThrows;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.WrittenBookContentComponent;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.DisconnectionInfo;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.ServerMetadata;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.RawFilteredPair;
import net.minecraft.text.Text;
import net.minecraft.util.Uuids;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
public class Listener {
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy/MM/dd");
private final SFCraft core;
public Listener(SFCraft core) {
@@ -20,22 +35,104 @@ public class Listener {
@SneakyThrows
public boolean onPlayerLogin(String currentPlayer, ClientConnection connection, Consumer<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 hourOfDay = ZonedDateTime.now(clock).getHour();
var startHr = SFCraft.getInstance().getConfig().maintainceStartHour;
var endHr = SFCraft.getInstance().getConfig().maintainceEndHour;
if (startHr > hourOfDay && hourOfDay > endHr) {
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, startHr, endHr)));
return false;
disconnect.accept(Text.of(String.format(msg, config.maintainceStartHour, config.maintainceEndHour)));
}
var canJoin = core.getAccessController().checkAccess(currentPlayer, address.getAddress().getHostAddress());
if (!canJoin) {
final var msg = "";
disconnect.accept(Text.of(String.format(msg, startHr, endHr)));
} catch (Exception ignored) {
System.err.println("Failed to locate " + address.getAddress());
}
}
return true;
return wl.isAllowed(offlineProfile);
}
public void onPlayerDeath(ServerPlayerEntity player, DamageSource damageSource) {
if (Helper.canBack(player)) {
player.sendMessage(Text.of("Tip: 死亡地点附近有玩家,可以使用 /back 传送到他们那里。(即使在对方也死亡的状态下)"));
}
}
public void onPlayerJoin(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender
packetSender, MinecraftServer minecraftServer) {
var player = serverPlayNetworkHandler.getPlayer();
if (!player.getCommandTags().contains("has_joined_before")) {
player.addCommandTag("has_joined_before");
player.sendMessage(Text.of("欢迎加入 SaltedFish Club Server!"));
player.sendMessage(Text.of("您受本服务器成员邀请并第一次加入本服务器,以下是一些注意事项:"));
player.sendMessage(Text.of(" 1. 服务器内有 base,但我们也鼓励您自立门户"));
player.sendMessage(Text.of(" 2. 服务器 QQ 群: (qq)"));
player.sendMessage(Text.of(" 3. 下午 " + SFCraft.getInstance().getConfig().maintainceStartHour + " ~ " + SFCraft.getInstance().getConfig().maintainceEndHour + " 为服务器维护时间(防沉迷),在这段时间内无法加入服务器。"));
player.sendMessage(Text.of("该时间由您 IP 属地的时区决定。我们不希望您的下午仅仅荒废在服务器上。"));
player.sendMessage(Text.of("如果您想邀请朋友加入服务器,可以向管理员提出申请。本服务器为正版服,但是离线玩家也可以加入(需备注)。"));
player.sendMessage(Text.of("为了账号安全起见,请尽量使用正版账号登录后加入。此外,正/盗同名将会被视为两个不同的玩家存档,数据不互通。"));
player.giveItemStack(generateHelperBook(core.getUpdateLog(), player));
unlockRecipe(player);
}
}
private ItemStack generateHelperBook(String updateLog, ServerPlayerEntity player) {
Clock clock = null;
if (player.networkHandler.getConnectionAddress() instanceof InetSocketAddress addr) {
try {
clock = SFCraft.getInstance().getGeoIPService().clockOf(addr.getAddress());
} catch (GeoIp2Exception e) {
clock = Clock.systemDefaultZone();
}
}
var book = new ItemStack(Items.WRITTEN_BOOK);
var updLog = new ArrayList<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;
}
}
}
@@ -0,0 +1,49 @@
package io.ib67.sfcraft;
import lombok.Getter;
import net.minecraft.entity.Entity;
import java.util.Objects;
/**
* 基于 Command Tags 实现的权限系统
* 权限节点通常以 `.` 切割并且总是在 `sfcraft` 分类下,例如 `sfcraft.back`
* 用 `-权限节点` 表示禁止使用
*
* @param <T> 对象
*/
public record Permission<T extends Entity>(String key, boolean byDefault) {
/**
* @param key 权限节点
*/
public Permission(String key, boolean byDefault) {
this.key = Objects.requireNonNull(key).trim().toLowerCase();
this.byDefault = byDefault;
}
public boolean hasPermission(T t) {
if (t.getCommandTags().contains(SFConsts.SPECIAL_SUDO)) return true;
if (byDefault) {
return !t.getCommandTags().contains("-" + key);
}
return t.getCommandTags().contains(key);
}
public void grant(T t) {
if (!hasPermission(t)) {
t.getCommandTags().remove("-" + key);
if (!byDefault) {
t.addCommandTag(key);
}
}
}
public void revoke(T t) {
if (hasPermission(t)) {
t.removeCommandTag(key);
if (byDefault) {
t.addCommandTag("-" + key);
}
}
}
}
@@ -1,6 +0,0 @@
package io.ib67.sfcraft;
public class SFConfig {
public int maintainceStartHour = 17;
public int maintainceEndHour = 1;
}
@@ -0,0 +1,21 @@
package io.ib67.sfcraft;
import lombok.experimental.UtilityClass;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
@UtilityClass
public class SFConsts {
public static final String SPECIAL_SUDO = "sfcraft.special.state.sudo";
public static final Permission<PlayerEntity> COMMAND_BACK = ofSFCPermission("command.home", true);
public static final Permission<PlayerEntity> UNLIMITED_COMMAND_BACK = ofSFCPermission("command.home.unlimited", true);
public static final Permission<PlayerEntity> COMMAND_ADDWL = ofSFCPermission("command.addwl", false);
public static final Permission<PlayerEntity> COMMAND_UNBLOCKSERVER = ofSFCPermission("command.unblockserver", false);
public static final Permission<PlayerEntity> COMMAND_LISTOFFLINE = ofSFCPermission("command.listoffline", false);
public static final Permission<PlayerEntity> COMMAND_LISTPERM = ofSFCPermission("command.listperm", false);
public static Permission<PlayerEntity> ofSFCPermission(String key, boolean byDefault) {
return new Permission<>("sfcraft." + key, byDefault);
}
}
+70 -53
View File
@@ -5,15 +5,14 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import io.ib67.sfcraft.access.AccessController;
import io.ib67.sfcraft.access.GrantedEntry;
import io.ib67.sfcraft.access.impl.SimpleAccessController;
import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.geoip.GeoIPService;
import lombok.Getter;
import lombok.SneakyThrows;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.CommandManager;
@@ -21,93 +20,111 @@ import net.minecraft.server.command.ServerCommandSource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
public class SFCraft implements ModInitializer {
@Getter
private SFConfig config;
@Getter
private static Listener listener;
private Listener listener;
@Getter
private static SFCraft instance;
@Getter
private GeoIPService geoIPService;
@Getter
private AccessController accessController;
@Getter
private Path root;
@Getter
private MinecraftServer server;
@Getter
private String motd;
@Getter
private String updateLog;
@Override
@SneakyThrows
public void onInitialize() {
instance = this;
listener = new Listener(this);
root = Path.of("sfcraft");
if (Files.notExists(root)) {
Files.createDirectory(root);
}
loadConfig();
listener = new Listener(this);
config = loadConfig();
motd = loadMotd();
updateLog = loadUpdateLog();
geoIPService = new GeoIPService();
accessController = loadAccessController();
ServerPlayConnectionEvents.JOIN.register(listener::onPlayerJoin);
CommandRegistrationCallback.EVENT.register(this::registerCommands);
ServerLifecycleEvents.SERVER_STARTED.register(this::onServerStarted);
}
private void onServerStarted(MinecraftServer minecraftServer) {
server = minecraftServer;
server.setMotd(motd);
}
@SneakyThrows
private void loadConfig() {
private String loadUpdateLog() {
var log = root.resolve("update.log");
if (Files.notExists(log)) {
return "他没写呢";
}
return Files.readString(log);
}
@SneakyThrows
private String loadMotd() {
var motd = root.resolve("motd.txt");
if (Files.notExists(motd)) {
Files.writeString(motd, "§bSaltedFish Club Server");
}
return Files.readString(motd);
}
@SneakyThrows
private SFConfig loadConfig() {
var gson = new Gson();
var cfgFile = root.resolve("config.json");
if (Files.notExists(cfgFile)) {
Files.writeString(cfgFile, gson.toJson(new SFConfig()));
config = new SFConfig();
return;
return new SFConfig();
}
config = gson.fromJson(Files.readString(cfgFile), SFConfig.class);
}
@SneakyThrows
private AccessController loadAccessController() {
var file = root.resolve("access.txt");
ServerLifecycleEvents.SERVER_STOPPING.register(this::saveAccessRules);
if (Files.notExists(file)) {
return new SimpleAccessController(List.of());
}
return new SimpleAccessController(Files.readAllLines(file).stream().map(it -> it.split(":")).filter(it -> it.length == 2).map(it -> new GrantedEntry(it[0].trim(), it[1].trim())).toList());
}
@SneakyThrows
private void saveAccessRules(MinecraftServer minecraftServer) {
var file = root.resolve("access.txt");
Files.writeString(file, accessController.getEntries().stream().map(i -> i.name() + ":" + i.condition()).collect(Collectors.joining("\n")));
return gson.fromJson(Files.readString(cfgFile), SFConfig.class);
}
private void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
var accessControl = new Commands.AccessControl(accessController);
var handler = new Commands();
// 一般非玩家实体都有 >= 2 的 PermissionLevel,因此不需要额外检查。
dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("access")
.requires(it -> it.hasPermissionLevel(2))
.then(LiteralArgumentBuilder.<ServerCommandSource>literal("grant")
.then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("name", StringArgumentType.string()).executes(it -> {
accessControl.grantAccess(it.getSource(), it.getArgument("name", String.class), null);
return 0;
}).then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("ip", StringArgumentType.string()).executes(it -> {
accessControl.grantAccess(it.getSource(), it.getArgument("name", String.class), it.getArgument("ip", String.class));
return 0;
}))))
.then(LiteralArgumentBuilder.<ServerCommandSource>literal("list")
.executes(it -> {
accessControl.listAccesses(it.getSource());
return 0;
})
).then(LiteralArgumentBuilder.<ServerCommandSource>literal("revoke")
.then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("name", StringArgumentType.string())
.executes(it -> {
accessControl.revokeAccess(it.getSource(), it.getArgument("name", String.class));
return 0;
}))
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)
);
}
}
@@ -1,18 +0,0 @@
package io.ib67.sfcraft.access;
import com.mojang.authlib.GameProfile;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.List;
public interface AccessController {
boolean checkAccess(String name, String address);
void grantAccess(String name, String condition);
void revokeAccess(String name);
Collection<? extends GrantedEntry> getEntries();
}
@@ -1,8 +0,0 @@
package io.ib67.sfcraft.access;
import com.mojang.authlib.GameProfile;
import java.net.InetSocketAddress;
public record GrantedEntry(String name, String condition) {
}
@@ -1,42 +0,0 @@
package io.ib67.sfcraft.access.impl;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import io.ib67.sfcraft.access.AccessController;
import io.ib67.sfcraft.access.GrantedEntry;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class SimpleAccessController implements AccessController {
private final Multimap<String, String> whitelist = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new);
public SimpleAccessController(List<GrantedEntry> entries) {
entries.forEach(i -> whitelist.put(i.name(), i.condition()));
}
@Override
public boolean checkAccess(String profile, String address) {
return whitelist.containsEntry(profile, address) || whitelist.containsEntry(profile, null);
}
@Override
public void grantAccess(String profile, String condition) {
whitelist.put(profile, condition);
}
@Override
public void revokeAccess(String profile) {
whitelist.removeAll(profile);
}
@Override
public Collection<? extends GrantedEntry> getEntries() {
return whitelist.asMap().entrySet()
.stream().flatMap(it -> it.getValue().stream().map(i -> new GrantedEntry(it.getKey(), i)))
.collect(Collectors.toSet());
}
}
@@ -0,0 +1,19 @@
package io.ib67.sfcraft.config;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.List;
public class SFConfig {
public int maintainceStartHour = 13;
public int maintainceEndHour = 17;
public boolean disableTemporarily;
public boolean isClosed(Clock clock) {
if (disableTemporarily) return false;
var hourOfDay = ZonedDateTime.now(clock).getHour();
var startHr = maintainceStartHour;
var endHr = maintainceEndHour;
return startHr < hourOfDay && hourOfDay < endHr;
}
}
@@ -1,5 +1,6 @@
package io.ib67.sfcraft.geoip;
import com.maxmind.db.CHMCache;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.record.Country;
@@ -26,8 +27,8 @@ import static java.net.http.HttpResponse.BodyHandlers.ofFile;
@Log4j2
public class GeoIPService implements Closeable {
private static final URI GEOIP_DOWNLOAD_URL = URI.create("https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb");
private static final Path COUNTRY_MMDB_PATH = SFCraft.getInstance().getRoot().resolve("country.mmdb");
private static final URI GEOIP_DOWNLOAD_URL = URI.create("http://sfclub.cc/GeoLite2-City.mmdb");
private static final Path COUNTRY_MMDB_PATH = SFCraft.getInstance().getRoot().resolve("GeoLite2-City.mmdb");
private final DatabaseReader databaseReader;
public GeoIPService() throws InterruptedException {
@@ -36,12 +37,12 @@ public class GeoIPService implements Closeable {
@SneakyThrows
public Clock clockOf(InetAddress address) throws GeoIp2Exception {
return Clock.system(TimeZone.getTimeZone(databaseReader.country(address).getCountry().getIsoCode()).toZoneId());
return Clock.system(TimeZone.getTimeZone(databaseReader.city(address).getLocation().getTimeZone()).toZoneId());
}
private DatabaseReader loadData() throws InterruptedException {
try {
return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).build();
return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).withCache(new CHMCache()).build();
} catch (IOException ignored) {
return updateData();
}
@@ -0,0 +1,42 @@
package io.ib67.sfcraft.mixin.server;
import io.ib67.sfcraft.Helper;
import io.netty.channel.ChannelHandlerContext;
import net.minecraft.network.QueryableServer;
import net.minecraft.network.handler.LegacyQueryHandler;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Locale;
@Mixin(LegacyQueryHandler.class)
public class LegacyQueryHandlerMixin {
SocketAddress address;
@Inject(method = "channelRead", at = @At("HEAD"))
public void onChannelRead(ChannelHandlerContext ctx, Object msg, CallbackInfo ci) {
address = ctx.channel().remoteAddress();
}
@Redirect(method = "channelRead", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/handler/LegacyQueryHandler;getResponse(Lnet/minecraft/network/QueryableServer;)Ljava/lang/String;"))
private String getResponse(QueryableServer server) {
var version = server.getVersion();
if (address instanceof InetSocketAddress address) {
version = Helper.getVersionString(address);
}
return String.format(
Locale.ROOT,
"§1\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d",
127,
version,
server.getServerMotd(),
server.getCurrentPlayerCount(),
server.getMaxPlayerCount()
);
}
}
@@ -0,0 +1,18 @@
package io.ib67.sfcraft.mixin.server;
import io.ib67.sfcraft.SFCraft;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerPlayerEntity.class)
public class PlayerEntityMixin {
@Inject(method = "onDeath", at = @At("TAIL"))
public void onDeath(DamageSource damageSource, CallbackInfo ci) {
SFCraft.getInstance().getListener().onPlayerDeath((ServerPlayerEntity) (Object) this, damageSource);
}
}
@@ -0,0 +1,24 @@
package io.ib67.sfcraft.mixin.server;
import io.ib67.sfcraft.SFCraft;
import net.minecraft.network.ClientConnection;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.ServerMetadata;
import net.minecraft.server.network.ServerHandshakeNetworkHandler;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ServerHandshakeNetworkHandler.class)
public abstract class ServerHandshakeNetworkHandlerMixin {
@Shadow
@Final
private ClientConnection connection;
@Redirect(method = "onHandshake", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getServerMetadata()Lnet/minecraft/server/ServerMetadata;"))
public ServerMetadata getMetadata(MinecraftServer instance) {
return SFCraft.getInstance().getListener().onMotdPing(instance,connection);
}
}
@@ -26,7 +26,7 @@ public abstract class ServerLoginNetworkHandlerMixin {
@Redirect(method = "onHello", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/ClientConnection;isLocal()Z"))
public boolean isLocal(ClientConnection connection) {
return SFCraft.getListener().onPlayerLogin(currentPlayer, connection, this::disconnect);
return SFCraft.getInstance().getListener().onPlayerLogin(currentPlayer, connection, this::disconnect);
}
+3
View File
@@ -8,6 +8,9 @@
"client": [
],
"server": [
"server.ServerHandshakeNetworkHandlerMixin",
"server.LegacyQueryHandlerMixin",
"server.PlayerEntityMixin",
"server.ServerLoginNetworkHandlerMixin"
],
"injectors": {