basic implementations

This commit is contained in:
iceBear67 committed 2024-08-21 13:58:31 +08:00
1 parent 4dbc691d93
commit 96dbc0ea65
34 files changed
+1051 -700

No files matched your search

@@ -8,15 +8,19 @@ import io.ib67.sfcraft.module.chat.ChatPingModule;
import io.ib67.sfcraft.module.chat.ChatSendLocModule;
import io.ib67.sfcraft.module.compat.ModCompatModule;
import io.ib67.sfcraft.module.randomevt.LongNightModule;
import io.ib67.sfcraft.module.room.CreativeRoomModule;
import io.ib67.sfcraft.registry.RoomRegistry;
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.registry.event.RandomEventRegistry;
import io.ib67.sfcraft.registry.RandomEventRegistry;
import io.ib67.sfcraft.module.command.BackModule;
import io.ib67.sfcraft.module.command.ManagementModule;
import io.ib67.sfcraft.registry.room.SimpleRoomRegistry;
import io.ib67.sfcraft.room.RoomTeleporter;
import lombok.extern.log4j.Log4j2;
import net.minecraft.server.MinecraftServer;
@@ -35,6 +39,8 @@ public class SFCraftInitializer extends GuiceModInitializer {
binder().bind(GeoIPService.class).to(MaxMindGeoIPService.class);
binder().bind(RandomEventRegistry.class).to(SFRandomEventRegistry.class);
binder().bind(SimpleMessageDecorator.class).in(Singleton.class);
binder().bind(RoomTeleporter.class).in(Singleton.class);
binder().bind(RoomRegistry.class).to(SimpleRoomRegistry.class).in(Singleton.class);
}
private void registerFeatures() {
@@ -51,6 +57,8 @@ public class SFCraftInitializer extends GuiceModInitializer {
registerFeature(ChatSendLocModule.class);
registerFeature(LongNightModule.class);
registerFeature(ModCompatModule.class);
registerFeature(RoomModule.class);
registerFeature(CreativeRoomModule.class);
}
@Override
@@ -1,14 +1,19 @@
package io.ib67.sfcraft.config;
import org.apache.commons.lang3.RandomStringUtils;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class SFConfig {
public boolean enableOfflineExempt = true;
public int maintainceStartHour = 13;
public int maintainceEndHour = 17;
public boolean disableTemporarily;
public String domain = "localhost";
public String serverSecret = RandomStringUtils.random(32);
public boolean isClosed(Clock clock) {
if (disableTemporarily) return false;
@@ -0,0 +1,37 @@
package io.ib67.sfcraft.mixin.server.subserver;
import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.util.Helper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.world.TeleportTarget;
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.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.UUID;
@Mixin(Entity.class)
public abstract class EntityMixin {
@Shadow
public abstract UUID getUuid();
@Inject(at = @At("HEAD"), method = "teleportTo", cancellable = true)
private void sf$redirectTeleport(TeleportTarget teleportTarget, CallbackInfoReturnable<Entity> cir) {
if (Helper.isVirtual(this.getUuid())) {
cir.setReturnValue((Entity) (Object) this);
}
}
@Inject(at = @At("HEAD"), method = "canUsePortals", cancellable = true)
private void sf$disablePortal(boolean allowVehicles, CallbackInfoReturnable<Boolean> cir) {
var registry = SFCraft.getInjector().getInstance(RoomRegistry.class);
var $this = (Entity) (Object) this;
if (registry.isRoomWorld($this.getWorld().getRegistryKey())) {
cir.setReturnValue(false);
}
}
}
@@ -0,0 +1,63 @@
package io.ib67.sfcraft.mixin.server.subserver;
import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.subserver.Room;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.ClientConnection;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.network.ConnectedClientData;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import org.slf4j.Logger;
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.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Optional;
@Mixin(PlayerManager.class)
public abstract class PlayerManagerMixin {
@Shadow
@Final
private static Logger LOGGER;
@Redirect(method = "onPlayerConnect", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayerEntity;setServerWorld(Lnet/minecraft/server/world/ServerWorld;)V"))
public void sf$modifyToRoomWorld(ServerPlayerEntity instance, ServerWorld world) {
var registry = SFCraft.getInjector().getInstance(RoomRegistry.class);
var room = registry.getRoomBy(instance.getUuid());
if (room != null) {
var pos = room.getSpawnPosition();
var nWorld = world.getServer().getWorld(pos.dimension());
instance.setServerWorld(nWorld);
var _pos = pos.pos();
instance.setPos(_pos.getX(), _pos.getY(), _pos.getZ());
} else {
instance.setServerWorld(world);
}
}
@Inject(method = "onPlayerConnect", at = @At("TAIL"))
public void sf$updateSession(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, CallbackInfo ci) {
var registry = SFCraft.getInjector().getInstance(RoomRegistry.class);
var room = registry.getRoomBy(player.getUuid());
if (room != null) {
LOGGER.info("Room player joined as " + player.getName().getLiteralString() + "/" + player.getUuid());
room.onPlayerJoin(player);
}
}
@Inject(method = "remove", at = @At("HEAD"))
public void sf$removePlayers(ServerPlayerEntity instance, CallbackInfo ci) {
var registry = SFCraft.getInjector().getInstance(RoomRegistry.class);
var room = registry.getRoomBy(instance.getUuid());
if (room != null) {
room.onPlayerQuit(instance);
}
}
}
@@ -0,0 +1,51 @@
package io.ib67.sfcraft.mixin.server.subserver;
import com.mojang.datafixers.DataFixer;
import io.ib67.sfcraft.util.Helper;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.WorldSavePath;
import net.minecraft.world.PlayerSaveHandler;
import net.minecraft.world.level.storage.LevelStorage;
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.io.File;
@Mixin(PlayerSaveHandler.class)
public class PlayerSaveHandlerMixin {
@Shadow
@Final
private File playerDataDir;
@Unique
private File virtualPlayerFolder;
@Inject(at = @At("TAIL"), method = "<init>")
private void sf$createVirtualPlayerDataFolder(LevelStorage.Session session, DataFixer dataFixer, CallbackInfo ci) {
this.virtualPlayerFolder = session.getDirectory(WorldSavePath.PLAYERDATA).resolve("in_rooms").toFile();
this.virtualPlayerFolder.mkdirs();
}
@Redirect(at = @At(value = "FIELD", target = "Lnet/minecraft/world/PlayerSaveHandler;playerDataDir:Ljava/io/File;"), method = "savePlayerData")
private File sf$getPlayerDataFolder(PlayerSaveHandler instance, PlayerEntity entity) {
if (Helper.isVirtual(entity.getUuid())) {
return virtualPlayerFolder;
} else {
return this.playerDataDir;
}
}
@Redirect(at = @At(value = "FIELD", target = "Lnet/minecraft/world/PlayerSaveHandler;playerDataDir:Ljava/io/File;"), method = "loadPlayerData(Lnet/minecraft/entity/player/PlayerEntity;Ljava/lang/String;)Ljava/util/Optional;")
private File sf$saveGetPlayerFolder(PlayerSaveHandler instance, PlayerEntity player) {
if (Helper.isVirtual(player.getUuid())) {
return virtualPlayerFolder;
} else {
return this.playerDataDir;
}
}
}
@@ -0,0 +1,132 @@
package io.ib67.sfcraft.mixin.server.subserver;
import com.mojang.authlib.GameProfile;
import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.module.RoomModule;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.room.CookieState;
import io.ib67.sfcraft.room.RequestedRoom;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.c2s.common.CookieResponseC2SPacket;
import net.minecraft.network.packet.s2c.common.CookieRequestS2CPacket;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.network.ServerLoginNetworkHandler;
import net.minecraft.text.Text;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
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.SocketAddress;
import static io.ib67.sfcraft.room.CookieState.*;
import static io.ib67.sfcraft.room.RoomTeleporter.ROOM_COOKIE;
@Mixin(ServerLoginNetworkHandler.class)
public abstract class ServerLoginNetworkHandlerMixin {
@Shadow
abstract void startVerify(GameProfile profile);
@Shadow
private @Nullable GameProfile profile;
@Shadow
@Final
private boolean transferred;
@Shadow
@Final
private ClientConnection connection;
@Shadow
public abstract void disconnect(Text reason);
@Shadow
@Final
private static Logger LOGGER;
@Unique
private CookieState sf$cookieState = CookieState.NOT_SENT;
@Unique
private RequestedRoom sf$room = null;
@Unique
private boolean clean;
@Inject(method = "tick", at = @At("TAIL"))
void restoreVerify(CallbackInfo ci) {
if ((sf$cookieState == SENT && sf$room != null) || (clean && sf$cookieState != DONE)) {
sf$cookieState = CookieState.RECV;
startVerify(this.profile);
}
}
@Inject(method = "onCookieResponse", at = @At("HEAD"), cancellable = true)
private void sf$onRoomId(CookieResponseC2SPacket packet, CallbackInfo ci) {
if (packet.key().equals(ROOM_COOKIE)) {
if (sf$cookieState != SENT) throw new IllegalStateException("Protocol error");
var roomSvc = SFCraft.getInjector().getInstance(RoomModule.class);
try {
if (packet.payload().length == 0) {
// clean reconnect.
LOGGER.info("Player " + profile.getName() + " requested a clean reconnection");
clean = true;
ci.cancel();
return;
}
this.sf$room = roomSvc.readRoomRequest(packet.payload());
} catch (Exception t) {
LOGGER.error("Failed to read cookie: {0}", t);
this.disconnect(Text.of("Protocol error."));
ci.cancel();
return;
}
}
ci.cancel();
}
@Inject(method = "startVerify", at = @At("HEAD"), cancellable = true)
void sf$makeTransferPlayerProfile(GameProfile profile, CallbackInfo ci) {
if (transferred && !clean) {
switch (this.sf$cookieState) {
case NOT_SENT -> {
this.profile = profile;
this.connection.send(new CookieRequestS2CPacket(ROOM_COOKIE));
this.sf$cookieState = SENT;
}
case RECV -> {
var ssr = SFCraft.getInjector().getInstance(RoomRegistry.class);
var room = ssr.getRoomBy(sf$room.identifier());
if (room != null) {
this.profile = new GameProfile(sf$room.profileUuid(), sf$room.profileName());
var session = room.getPlayerManager().getSessionBy(sf$room.profileUuid());
if (session == null) {
disconnect(Text.of("Session isn't exists."));
ci.cancel();
return;
}
session.onPlayerLogin(sf$room.profileUuid());
this.sf$cookieState = DONE;
this.startVerify(this.profile);
ci.cancel();
return;
}
}
}
if (this.sf$cookieState != CookieState.DONE) {
ci.cancel();
}
}
if (clean) {
this.sf$cookieState = DONE;
}
}
@Redirect(method = "tickVerify", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/PlayerManager;checkCanJoin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/text/Text;"))
private Text sf$bypassRoomPlayer(PlayerManager instance, SocketAddress address, GameProfile profile) {
if (sf$cookieState == DONE) return null;
return instance.checkCanJoin(address, profile);
}
}
@@ -0,0 +1,44 @@
package io.ib67.sfcraft.mixin.server.subserver;
import io.ib67.sfcraft.SFCraft;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.util.Helper;
import net.minecraft.network.packet.s2c.play.PositionFlag;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Set;
@Mixin(ServerPlayerEntity.class)
public abstract class ServerPlayerEntityMixin {
@Inject(at = @At("HEAD"), method = "teleport(Lnet/minecraft/server/world/ServerWorld;DDDLjava/util/Set;FF)Z", cancellable = true)
public void sf$interceptTeleport(ServerWorld world, double destX, double destY, double destZ, Set<PositionFlag> flags, float yaw, float pitch, CallbackInfoReturnable<Boolean> cir) {
if (Helper.isVirtual($this().getUuid())) {
if (world != $this().getServerWorld()) {
cir.setReturnValue(false);
}
}
}
@Inject(at = @At("HEAD"), method = "teleport(Lnet/minecraft/server/world/ServerWorld;DDDFF)V", cancellable = true)
public void sf$interceptTeleport(ServerWorld targetWorld, double x, double y, double z, float yaw, float pitch, CallbackInfo ci) {
if (Helper.isVirtual($this().getUuid())) {
if (targetWorld != $this().getServerWorld()) {
ci.cancel();
}
}
}
private RoomRegistry roomRegistry() {
return SFCraft.getInjector().getInstance(RoomRegistry.class);
}
private ServerPlayerEntity $this() {
return (ServerPlayerEntity) (Object) this;
}
}
@@ -0,0 +1,164 @@
package io.ib67.sfcraft.module;
import com.google.inject.Inject;
import com.mojang.authlib.GameProfile;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import io.ib67.sfcraft.ServerModule;
import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.inject.MinecraftServerSupplier;
import io.ib67.sfcraft.room.RequestedRoom;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.util.Helper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.network.PacketCallbacks;
import net.minecraft.network.codec.PacketCodecs;
import net.minecraft.network.packet.s2c.common.ServerTransferS2CPacket;
import net.minecraft.network.packet.s2c.common.StoreCookieS2CPacket;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.world.ChunkTicket;
import net.minecraft.server.world.ChunkTicketType;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.Pair;
import net.minecraft.util.Unit;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
import org.apache.commons.lang3.RandomStringUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.List;
import static io.ib67.sfcraft.room.RoomTeleporter.ROOM_COOKIE;
import static io.ib67.sfcraft.util.SFConsts.COMMAND_RECO;
@Log4j2
public class RoomModule extends ServerModule {
private static final byte[] EMPTY = new byte[0];
@Inject
private RoomRegistry roomRegistry;
@Inject
private MinecraftServerSupplier serverSupplier;
@Inject
private SFConfig config;
private Key key;
private final List<Pair<RegistryKey<World>, BlockPos>> pregenQueue = new ArrayList<>();
@Override
public void onInitialize() {
key = getKeyFromPassword(config.serverSecret, RandomStringUtils.random(16));
CommandRegistrationCallback.EVENT.register(this::registerCommand);
}
@Override
public void onEnable() {
for (Pair<RegistryKey<World>, BlockPos> registryKeyBlockPosPair : pregenQueue) {
var world = serverSupplier.get().getWorld(registryKeyBlockPosPair.getLeft());
if(world == null){
log.warn("Cannot find world "+registryKeyBlockPosPair.getLeft());
continue;
}
world.getChunkManager().addTicket(ChunkTicketType.START,new ChunkPos(registryKeyBlockPosPair.getRight()),64, Unit.INSTANCE);
}
}
private void registerCommand(CommandDispatcher<ServerCommandSource> serverCommandSourceCommandDispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
serverCommandSourceCommandDispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("reco")
.requires(it -> it.isExecutedByPlayer() && COMMAND_RECO.hasPermission(it.getPlayer()))
.executes(this::onCleanReconnect)
);
}
private int onCleanReconnect(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) {
var player = serverCommandSourceCommandContext.getSource().getPlayer();
if (!Helper.isVirtual(player.getUuid())) {
player.sendMessage(Text.literal("你不在任何 \"房间\" 内。"));
return 0;
}
var networkHandler = player.networkHandler;
networkHandler.send(new StoreCookieS2CPacket(ROOM_COOKIE, EMPTY), PacketCallbacks.always(() -> {
networkHandler.sendPacket(new ServerTransferS2CPacket(config.domain, serverSupplier.get().getServerPort()));
}));
return 0;
}
@SneakyThrows
public byte[] encrypt(byte[] b) {
var cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(b);
}
@SneakyThrows
public byte[] decrypt(byte[] b) {
var cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(b);
}
public byte[] serializeRequestedRoom(RequestedRoom room) {
var buf = Unpooled.buffer();
ByteBuf nbuf = null;
try {
Identifier.PACKET_CODEC.encode(buf, room.identifier());
PacketCodecs.GAME_PROFILE.encode(buf, new GameProfile(room.profileUuid(), room.profileName()));
var encrypted = encrypt(buf.array());
nbuf = Unpooled.buffer();
nbuf.writeShortLE(encrypted.length);
nbuf.writeBytes(encrypted);
return nbuf.array();
} finally {
if (nbuf != null) nbuf.release();
buf.release();
}
}
public RequestedRoom readRoomRequest(byte[] payload) {
ByteBuf buf = null;
ByteBuf decrypted = null;
try {
buf = Unpooled.wrappedBuffer(payload);
var len = buf.readUnsignedShortLE();
var content = new byte[len];
buf.readBytes(content);
decrypted = Unpooled.wrappedBuffer(decrypt(content));
var identifier = Identifier.PACKET_CODEC.decode(decrypted);
var profile = PacketCodecs.GAME_PROFILE.decode(decrypted);
return new RequestedRoom(identifier, profile.getName(), profile.getId());
} finally {
if (buf != null) buf.release(); //todo
if (decrypted != null) decrypted.release();
}
}
@SneakyThrows
private static SecretKey getKeyFromPassword(String password, String salt) {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
.getEncoded(), "AES");
return secret;
}
public void enqueuePregen(RegistryKey<World> world, BlockPos spawnPos) {
pregenQueue.add(new Pair<>(world, spawnPos));
}
}
@@ -6,8 +6,7 @@ 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.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import io.ib67.sfcraft.registry.RandomEventRegistry;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
@@ -0,0 +1,57 @@
package io.ib67.sfcraft.module.room;
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.ServerModule;
import io.ib67.sfcraft.inject.MinecraftServerSupplier;
import io.ib67.sfcraft.module.RoomModule;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.room.RoomTeleporter;
import io.ib67.sfcraft.room.create.CreativeSpaceFactory;
import io.ib67.sfcraft.room.create.CreativeSpaceRoom;
import io.ib67.sfcraft.util.SFConsts;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.world.chunk.ChunkStatus;
public class CreativeRoomModule extends ServerModule {
@Inject
private RoomRegistry roomRegistry;
@Inject
private RoomTeleporter teleporter;
@Inject
private RoomModule roomModule;
@Inject
private MinecraftServerSupplier serverSupplier;
private CreativeSpaceRoom room;
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT
.register(this::registerCommand);
room = new CreativeSpaceRoom();
roomRegistry.registerRoomType(CreativeSpaceRoom.class, CreativeSpaceRoom.WORLD, new CreativeSpaceFactory(room));
roomRegistry.createRoomOf(CreativeSpaceRoom.class, CreativeSpaceRoom.IDENTIFIER, null, null);
roomModule.enqueuePregen(CreativeSpaceRoom.WORLD,CreativeSpaceRoom.SPAWN_POS);
}
private void registerCommand(CommandDispatcher<ServerCommandSource> dispatcher,
CommandRegistryAccess registry, CommandManager.RegistrationEnvironment env) {
dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("playground")
.requires(it -> it.isExecutedByPlayer() && SFConsts.COMMAND_PLAYGROUND.hasPermission(it.getPlayer()))
.executes(this::gotoPlayground)
);
}
private int gotoPlayground(CommandContext<ServerCommandSource> serverCommandSourceCommandContext) {
var player = serverCommandSourceCommandContext.getSource().getPlayer();
teleporter.teleportTo(room, player);
return 0;
}
}
@@ -1,4 +1,4 @@
package io.ib67.sfcraft.registry.event;
package io.ib67.sfcraft.registry;
import io.ib67.sfcraft.module.randomevt.ActiveEvent;
import io.ib67.sfcraft.module.randomevt.RandomEvent;
@@ -0,0 +1,26 @@
package io.ib67.sfcraft.registry;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.subserver.RoomFactory;
import io.ib67.sfcraft.subserver.RoomSession;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.world.World;
import java.util.Collection;
import java.util.UUID;
public interface RoomRegistry {
Collection<? extends Room> getRooms();
Room getRoomBy(Identifier identifier);
RoomSession getRoomBy(UUID player);
<T extends Room> T createRoomOf(Class<T> type, Identifier roomId, ServerPlayerEntity issuer, String... arguments);
boolean isRoomWorld(RegistryKey<World> world);
<T extends Room> void registerRoomType(Class<T> type, RegistryKey<World> worldKey, RoomFactory<T> factory);
}
@@ -2,6 +2,7 @@ package io.ib67.sfcraft.registry.event;
import io.ib67.sfcraft.module.randomevt.ActiveEvent;
import io.ib67.sfcraft.module.randomevt.RandomEvent;
import io.ib67.sfcraft.registry.RandomEventRegistry;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
@@ -0,0 +1,71 @@
package io.ib67.sfcraft.registry.room;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.subserver.RoomFactory;
import io.ib67.sfcraft.subserver.RoomSession;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.world.World;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SimpleRoomRegistry implements RoomRegistry {
private final ConcurrentMap<Identifier, Room> rooms = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<Class<?>, RoomFactory<?>> factories = new HashMap<>();
private final Set<RegistryKey<World>> roomWorlds = new HashSet<>();
@Override
public Collection<? extends Room> getRooms() {
return rooms.values();
}
@Override
public Room getRoomBy(Identifier identifier) {
return rooms.get(identifier);
}
@Override
public RoomSession getRoomBy(UUID player) {
lock.readLock().lock();
try {
return rooms.values().stream()
.map(it -> it.getPlayerManager().getSessionBy(player))
.filter(Objects::nonNull)
.findFirst().orElse(null);
} finally {
lock.readLock().unlock();
}
}
@Override
public <T extends Room> T createRoomOf(Class<T> type, Identifier roomId, ServerPlayerEntity issuer, String... arguments) {
if (getRoomBy(roomId) != null) {
throw new IllegalArgumentException("duplicate room id: " + roomId);
}
lock.writeLock().lock();
try {
var room = factories.get(type).create(issuer, arguments);
rooms.put(roomId, room);
return (T) room;
} finally {
lock.writeLock().unlock();
}
}
@Override
public boolean isRoomWorld(RegistryKey<World> world) {
return roomWorlds.contains(world);
}
@Override
public <T extends Room> void registerRoomType(Class<T> type, RegistryKey<World> worldKey, RoomFactory<T> factory) {
roomWorlds.add(worldKey);
factories.put(type, factory);
}
}
@@ -0,0 +1,10 @@
package io.ib67.sfcraft.room;
public enum CookieState {
NOT_SENT,
SENT,
RECV,
DONE,
ERROR
}
@@ -0,0 +1,12 @@
package io.ib67.sfcraft.room;
import net.minecraft.util.Identifier;
import java.util.UUID;
public record RequestedRoom(
Identifier identifier,
String profileName,
UUID profileUuid
) {
}
@@ -0,0 +1,50 @@
package io.ib67.sfcraft.room;
import com.google.inject.Inject;
import io.ib67.sfcraft.config.SFConfig;
import io.ib67.sfcraft.inject.MinecraftServerSupplier;
import io.ib67.sfcraft.module.RoomModule;
import io.ib67.sfcraft.registry.RoomRegistry;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.util.Helper;
import net.minecraft.network.PacketCallbacks;
import net.minecraft.network.packet.s2c.common.ServerTransferS2CPacket;
import net.minecraft.network.packet.s2c.common.StoreCookieS2CPacket;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Colors;
import net.minecraft.util.Identifier;
public class RoomTeleporter {
@Inject
private RoomRegistry registry;
@Inject
private RoomModule roomModule;
@Inject
private MinecraftServerSupplier serverSupplier;
@Inject
private SFConfig config;
public static final Identifier ROOM_COOKIE = Identifier.of("sfcraft", "room");
public void teleportTo(Room room, ServerPlayerEntity player) {
if (Helper.isVirtual(player.getUuid())) {
player.sendMessage(Text.literal("您已经在某个房间里了").withColor(Colors.LIGHT_RED));
return;
}
var finalUuid = Helper.generateIdForRoom(player.getName().getLiteralString(), room.getServerIdentifier());
var sess = room.getPlayerManager().createSessionFor(finalUuid);
if(player.getServer().getWorld(sess.getSpawnPosition().dimension()) == null){
throw new IllegalStateException("world isn't exist");
}
var networkHandler = player.networkHandler;
networkHandler.reconfigure();
networkHandler.send(new StoreCookieS2CPacket(ROOM_COOKIE, roomModule.serializeRequestedRoom(new RequestedRoom(
room.getServerIdentifier(),
player.getName().getLiteralString(),
finalUuid
))), PacketCallbacks.always(() -> {
networkHandler.sendPacket(new ServerTransferS2CPacket(config.domain, serverSupplier.get().getServerPort()));
}));
}
}
@@ -0,0 +1,93 @@
package io.ib67.sfcraft.room;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.subserver.RoomPlayerManager;
import io.ib67.sfcraft.subserver.RoomSession;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.GlobalPos;
import java.util.*;
public abstract class SimpleRoomPlayerManager implements RoomPlayerManager {
protected final Map<UUID, PlayerEntry> onlinePlayers = new HashMap<>();
protected final Room room;
protected SimpleRoomPlayerManager(Room room) {
this.room = room;
}
@Override
public List<ServerPlayerEntity> getJoinedPlayers() {
return onlinePlayers.values().stream().map(it -> it.player).toList();
}
@Override
public List<RoomSession> getSessions() {
return onlinePlayers.values().stream().map(it -> it.session).toList();
}
@Override
public RoomSession createSessionFor(UUID uuid) {
return onlinePlayers.computeIfAbsent(uuid, this::makeSession).getSession();
}
private PlayerEntry makeSession(UUID uuid) {
var session = createSession(uuid);
return new PlayerEntry(null, new DelegatedSession(session));
}
protected abstract RoomSession createSession(UUID id);
@Override
public RoomSession getSessionBy(UUID uuid) {
var proc = onlinePlayers.get(uuid);
if (proc == null) return null;
return proc.session;
}
@Override
public ServerPlayerEntity getPlayer(UUID uuid) {
return onlinePlayers.get(uuid).getPlayer();
}
class DelegatedSession extends RoomSession {
private final RoomSession session;
protected DelegatedSession(RoomSession session) {
super(SimpleRoomPlayerManager.this.room);
this.session = session;
}
@Override
public GlobalPos getSpawnPosition() {
return session.getSpawnPosition();
}
@Override
public void onPlayerLogin(UUID uuid) {
session.onPlayerLogin(uuid);
}
@Override
public void onPlayerJoin(ServerPlayerEntity player) {
onlinePlayers.get(player.getUuid()).setPlayer(player);
session.onPlayerJoin(player);
}
@Override
public void onPlayerQuit(ServerPlayerEntity player) {
onlinePlayers.remove(player.getUuid());
session.onPlayerQuit(player);
}
}
@Data
@AllArgsConstructor
class PlayerEntry {
private ServerPlayerEntity player;
private RoomSession session;
}
}
@@ -0,0 +1,18 @@
package io.ib67.sfcraft.room.create;
import io.ib67.sfcraft.room.SimpleRoomPlayerManager;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.subserver.RoomSession;
import java.util.UUID;
public class CreativePlayerManager extends SimpleRoomPlayerManager {
protected CreativePlayerManager(Room room) {
super(room);
}
@Override
protected RoomSession createSession(UUID id) {
return new CreativePlayerSession(room);
}
}
@@ -0,0 +1,47 @@
package io.ib67.sfcraft.room.create;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.subserver.RoomSession;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Colors;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.GlobalPos;
import net.minecraft.world.GameMode;
import java.util.UUID;
public class CreativePlayerSession extends RoomSession {
private final GlobalPos spawn;
protected CreativePlayerSession(Room room) {
super(room);
spawn = new GlobalPos(
CreativeSpaceRoom.WORLD,
CreativeSpaceRoom.SPAWN_POS
);
}
@Override
public void onPlayerJoin(ServerPlayerEntity player) {
player.sendMessage(Text.of("你现在正在创造游乐园中。"));
player.sendMessage(Text.literal("游乐园中的生物不能逃逸到其他维度。").withColor(Colors.GRAY));
player.sendMessage(Text.literal("使用 /reco 或重新加入游戏即可离开。").withColor(Colors.GRAY));
player.changeGameMode(GameMode.CREATIVE);
}
@Override
public GlobalPos getSpawnPosition() {
return spawn;
}
@Override
public void onPlayerLogin(UUID uuid) {
}
@Override
public void onPlayerQuit(ServerPlayerEntity player) {
}
}
@@ -0,0 +1,15 @@
package io.ib67.sfcraft.room.create;
import io.ib67.sfcraft.subserver.RoomFactory;
import lombok.RequiredArgsConstructor;
import net.minecraft.server.network.ServerPlayerEntity;
@RequiredArgsConstructor
public class CreativeSpaceFactory implements RoomFactory<CreativeSpaceRoom> {
private final CreativeSpaceRoom room;
@Override
public CreativeSpaceRoom create(ServerPlayerEntity issuer, String[] arguments) {
return room;
}
}
@@ -0,0 +1,36 @@
package io.ib67.sfcraft.room.create;
import io.ib67.sfcraft.subserver.Room;
import io.ib67.sfcraft.subserver.RoomPlayerManager;
import lombok.Getter;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
public class CreativeSpaceRoom implements Room {
public static final RegistryKey<World> WORLD = RegistryKey.of(RegistryKeys.WORLD, Identifier.of("sfcraft", "playground"));
public static final Identifier IDENTIFIER = Identifier.of("sfcraft", "creative_space");
public static final BlockPos SPAWN_POS = new BlockPos(0, 66, 0);
@Getter
private final RoomPlayerManager playerManager;
public CreativeSpaceRoom() {
this.playerManager = new CreativePlayerManager(this);
}
@Override
public void shutdown() {
for (ServerPlayerEntity joinedPlayer : playerManager.getJoinedPlayers()) {
joinedPlayer.networkHandler.disconnect(Text.of("Room is shutting down"));
}
}
@Override
public Identifier getServerIdentifier() {
return IDENTIFIER;
}
}
@@ -0,0 +1,17 @@
package io.ib67.sfcraft.subserver;
import net.minecraft.network.packet.s2c.common.StoreCookieS2CPacket;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.GlobalPos;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public interface Room {
Identifier getServerIdentifier();
RoomPlayerManager getPlayerManager();
void shutdown();
}
@@ -0,0 +1,8 @@
package io.ib67.sfcraft.subserver;
import net.minecraft.server.network.ServerPlayerEntity;
@FunctionalInterface
public interface RoomFactory<T extends Room> {
T create(ServerPlayerEntity issuer, String[] arguments);
}
@@ -0,0 +1,18 @@
package io.ib67.sfcraft.subserver;
import net.minecraft.server.network.ServerPlayerEntity;
import java.util.List;
import java.util.UUID;
public interface RoomPlayerManager {
List<ServerPlayerEntity> getJoinedPlayers();
List<RoomSession> getSessions();
RoomSession createSessionFor(UUID uuid);
RoomSession getSessionBy(UUID uuid);
ServerPlayerEntity getPlayer(UUID uuid);
}
@@ -0,0 +1,24 @@
package io.ib67.sfcraft.subserver;
import lombok.Getter;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.GlobalPos;
import java.util.UUID;
public abstract class RoomSession {
@Getter
protected final Room room;
protected RoomSession(Room room) {
this.room = room;
}
public abstract GlobalPos getSpawnPosition();
public abstract void onPlayerLogin(UUID uuid);
public abstract void onPlayerJoin(ServerPlayerEntity player);
public abstract void onPlayerQuit(ServerPlayerEntity player);
}
@@ -9,6 +9,7 @@ import net.minecraft.block.Block;
import net.minecraft.block.CropBlock;
import net.minecraft.block.Fertilizable;
import net.minecraft.block.SaplingBlock;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.fluid.EmptyFluid;
import net.minecraft.fluid.LavaFluid;
import net.minecraft.fluid.WaterFluid;
@@ -16,15 +17,18 @@ import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.Colors;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.UUID;
public class Helper {
public static final char COLOR = '§';
@@ -113,4 +117,13 @@ public class Helper {
}
return q[0] + "." + q[1] + ".*.*";
}
public static UUID generateIdForRoom(String name, Identifier room) {
var uuid = UUID.nameUUIDFromBytes((name + "@" + room.toString()).getBytes(StandardCharsets.UTF_8));
return new UUID(uuid.getMostSignificantBits(), 0);
}
public static boolean isVirtual(UUID uuid) {
return uuid.getLeastSignificantBits() == 0;
}
}
@@ -20,6 +20,8 @@ public class SFConsts {
public static final Permission<PlayerEntity> COMMAND_LISTOFFLINE = ofSFCPermission("command.listoffline", false);
public static final Permission<PlayerEntity> COMMAND_LISTPERM = ofSFCPermission("command.listperm", false);
public static final Permission<PlayerEntity> COMMAND_LISTGEO = ofSFCPermission("command.listgeo", false);
public static final Permission<PlayerEntity> COMMAND_PLAYGROUND = ofSFCPermission("command.playground", true);
public static final Permission<PlayerEntity> COMMAND_RECO = ofSFCPermission("command.reco", true);
public static Permission<PlayerEntity> ofSFCPermission(String key, boolean byDefault) {
return new Permission<>("sfcraft." + key, byDefault);
@@ -0,0 +1,16 @@
{
"type": "overworld",
"generator": {
"type": "flat",
"settings": {
"layers": [
{
"height": 64,
"block": "minecraft:white_stained_glass"
}
],
"structure_overrides": [],
"biome": "plains"
}
}
}
+6 -1
View File
@@ -26,7 +26,12 @@
"common.MobEntityMixin",
"server.hack.EntitySpawnS2CPacketMixin",
"server.hack.RegistryHandlerMixin",
"server.hack.HoverEntityContentMixin"
"server.hack.HoverEntityContentMixin",
"server.subserver.ServerLoginNetworkHandlerMixin",
"server.subserver.EntityMixin",
"server.subserver.PlayerManagerMixin",
"server.subserver.PlayerSaveHandlerMixin",
"server.subserver.ServerPlayerEntityMixin"
],
"injectors": {
"defaultRequire": 1