diff --git a/src/client/java/xyz/nucleoid/plasmid/client/impl/GameWorldCreator.java b/src/client/java/xyz/nucleoid/plasmid/client/impl/GameWorldCreator.java new file mode 100644 index 00000000..1ed13683 --- /dev/null +++ b/src/client/java/xyz/nucleoid/plasmid/client/impl/GameWorldCreator.java @@ -0,0 +1,78 @@ +package xyz.nucleoid.plasmid.client.impl; + +import com.mojang.serialization.Lifecycle; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.world.CreateWorldScreen; +import net.minecraft.client.gui.screen.world.InitialWorldOptions; +import net.minecraft.client.gui.screen.world.WorldCreator; +import net.minecraft.client.world.GeneratorOptionsHolder; +import net.minecraft.registry.ServerDynamicRegistryType; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.resource.featuretoggle.FeatureFlags; +import net.minecraft.world.Difficulty; +import net.minecraft.world.GameMode; +import net.minecraft.world.GameRules; +import net.minecraft.world.gen.FlatLevelGeneratorPresets; +import net.minecraft.world.level.LevelInfo; +import net.minecraft.world.level.LevelProperties; +import xyz.nucleoid.plasmid.api.game.config.GameConfig; +import xyz.nucleoid.plasmid.api.game.player.JoinIntent; +import xyz.nucleoid.plasmid.client.impl.screen.GamesScreen; +import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; +import xyz.nucleoid.plasmid.impl.game.manager.HasForcedGameSpace; + +import java.util.Set; + +public final class GameWorldCreator { + public static final String WORLD_NAME = "PlasmidGame"; + public static final InitialWorldOptions OPTIONS = new InitialWorldOptions(WorldCreator.Mode.SURVIVAL, Set.of(), FlatLevelGeneratorPresets.THE_VOID); + + private GameWorldCreator() { + } + + public static void create(MinecraftClient client, GeneratorOptionsHolder generatorOptionsHolder, RegistryEntry> game) { + var dimensionsConfig = generatorOptionsHolder.selectedDimensions() + .toConfig(generatorOptionsHolder.dimensionOptionsRegistry()); + + var registries = generatorOptionsHolder.combinedDynamicRegistries() + .with(ServerDynamicRegistryType.DIMENSIONS, dimensionsConfig.toDynamicRegistryManager()); + + var levelInfo = new LevelInfo( + WORLD_NAME, + GameMode.SURVIVAL, + false, + Difficulty.NORMAL, + false, + new GameRules(FeatureFlags.DEFAULT_ENABLED_FEATURES), + generatorOptionsHolder.dataConfiguration() + ); + + var levelProperties = new LevelProperties( + levelInfo, + generatorOptionsHolder.generatorOptions(), + dimensionsConfig.specialWorldProperty(), + Lifecycle.stable() + ); + + CreateWorldScreen.showMessage(client, GamesScreen.PREPARING_MESSAGE); + + var session = CreateWorldScreen.createSession(client, levelInfo.getLevelName(), null); + + client.createIntegratedServerLoader().startNewWorld( + session.orElseThrow(), + generatorOptionsHolder.dataPackContents(), + registries, + levelProperties + ); + + // Server is created by now, so a game space can be created + var server = client.getServer(); + var gameSpace = GameSpaceManagerImpl.get().open(game).join(); + + ((HasForcedGameSpace) server).setForcedGameSpace(gameSpace); + + // Add existing players in the server to the game space just in case + var players = server.getPlayerManager().getPlayerList(); + gameSpace.getPlayers().offer(players, JoinIntent.PLAY); + } +} diff --git a/src/client/java/xyz/nucleoid/plasmid/client/impl/PlasmidClient.java b/src/client/java/xyz/nucleoid/plasmid/client/impl/PlasmidClient.java new file mode 100644 index 00000000..43563df1 --- /dev/null +++ b/src/client/java/xyz/nucleoid/plasmid/client/impl/PlasmidClient.java @@ -0,0 +1,53 @@ +package xyz.nucleoid.plasmid.client.impl; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import xyz.nucleoid.plasmid.client.impl.screen.GamesScreen; + +public class PlasmidClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> { + if (screen instanceof TitleScreen) { + addTitleScreenButton(client, screen, width, height); + } + }); + } + + private static void addTitleScreenButton(MinecraftClient client, Screen screen, int width, int height) { + var buttons = Screens.getButtons(screen); + + int x = (width - ButtonWidget.field_49479) / 2; + int y = 0; + + int index = 0; + int insertIndex = 0; + + for (var button : buttons) { + if (button.getX() == x && button.getWidth() == ButtonWidget.field_49479) { + if (button.getY() > y) { + y = button.getY(); + insertIndex = index + 1; + } + + button.setY(button.getY() - 24); + } + + index += 1; + } + + var gamesButton = ButtonWidget.builder(GamesScreen.TITLE, button -> { + GamesScreen.show(client, screen); + }) + .position(x, y) + .width(ButtonWidget.field_49479) + .build(); + + buttons.add(insertIndex, gamesButton); + } +} diff --git a/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GameListEntry.java b/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GameListEntry.java new file mode 100644 index 00000000..fcd5b90d --- /dev/null +++ b/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GameListEntry.java @@ -0,0 +1,86 @@ +package xyz.nucleoid.plasmid.client.impl.screen; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget; +import net.minecraft.client.input.KeyCodes; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import xyz.nucleoid.plasmid.api.game.config.GameConfig; + +public class GameListEntry extends AlwaysSelectedEntryListWidget.Entry { + private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot"); + + protected final RegistryEntry> game; + + protected final Text name; + protected final Text description; + + private final GamesScreen screen; + private final TextRenderer textRenderer; + + private long lastClickTime; + + public GameListEntry(RegistryEntry> game, GamesScreen screen, TextRenderer textRenderer) { + this.game = game; + this.name = GameConfig.name(this.game); + + var description = this.game.value().description(); + this.description = description.isEmpty() ? null : ScreenTexts.joinLines(description); + + this.screen = screen; + this.textRenderer = textRenderer; + } + + private void activate() { + this.screen.client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1)); + + this.screen.setSelectedGame(this.game); + this.screen.play(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (Util.getMeasuringTimeMs() - this.lastClickTime >= Element.MAX_DOUBLE_CLICK_INTERVAL) { + this.lastClickTime = Util.getMeasuringTimeMs(); + return super.mouseClicked(mouseX, mouseY, button); + } + + this.activate(); + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (KeyCodes.isToggle(keyCode)) { + this.activate(); + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + context.drawGuiTexture(RenderLayer::getGuiTextured, SLOT_TEXTURE, x + 1, y + 1, 18, 18); + + context.drawItemWithoutEntity(this.game.value().icon(), x + 2, y + 2); + context.drawStackOverlay(this.textRenderer, this.game.value().icon(), x + 2, y + 2); + + context.drawTextWithShadow(this.textRenderer, this.name, x + 18 + 5, y + 6, Colors.WHITE); + } + + @Override + public Text getNarration() { + var contents = this.description == null ? this.name : ScreenTexts.joinSentences(this.name, this.description); + return Text.translatable("narrator.select", contents); + } +} diff --git a/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GameListWidget.java b/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GameListWidget.java new file mode 100644 index 00000000..29667748 --- /dev/null +++ b/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GameListWidget.java @@ -0,0 +1,72 @@ +package xyz.nucleoid.plasmid.client.impl.screen; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.AlwaysSelectedEntryListWidget; +import net.minecraft.registry.DynamicRegistryManager; +import net.minecraft.registry.Registry; +import net.minecraft.registry.entry.RegistryEntry; +import xyz.nucleoid.plasmid.api.game.config.GameConfig; +import xyz.nucleoid.plasmid.api.game.config.GameConfigs; + +import java.util.Comparator; +import java.util.Locale; + +public class GameListWidget extends AlwaysSelectedEntryListWidget { + private static final Comparator>> ENTRY_ORDERING = Comparator.comparing( + entry -> GameConfig.name(entry).getString(), + String::compareToIgnoreCase + ); + + private final Registry> registry; + private final GamesScreen screen; + + private String search = ""; + + public GameListWidget(DynamicRegistryManager registryManager, GamesScreen screen) { + super(screen.client, screen.width, screen.height - 85, 48, 24); + + this.registry = registryManager.getOrThrow(GameConfigs.REGISTRY_KEY); + this.screen = screen; + + this.updateEntries(); + } + + public void setSearch(String search) { + if (!this.search.equalsIgnoreCase(search)) { + this.search = search.toLowerCase(Locale.ROOT); + this.updateEntries(); + } + } + + private void updateEntries() { + this.clearEntries(); + + this.registry.streamEntries() + .filter(entry -> { + String name = GameConfig.name(entry).getString().toLowerCase(Locale.ROOT); + return name.contains(this.search); + }) + .sorted(ENTRY_ORDERING) + .map(entry -> new GameListEntry(entry, this.screen, this.client.textRenderer)) + .forEachOrdered(this::addEntry); + + this.refreshScroll(); + } + + @Override + public void setSelected(GameListEntry entry) { + super.setSelected(entry); + this.screen.setSelectedGame(entry == null ? null : entry.game); + } + + @Override + public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + super.renderWidget(context, mouseX, mouseY, delta); + + var entry = this.getHoveredEntry(); + + if (entry != null && entry.description != null) { + this.screen.setTooltip(entry.description); + } + } +} diff --git a/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GamesScreen.java b/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GamesScreen.java new file mode 100644 index 00000000..ff883567 --- /dev/null +++ b/src/client/java/xyz/nucleoid/plasmid/client/impl/screen/GamesScreen.java @@ -0,0 +1,150 @@ +package xyz.nucleoid.plasmid.client.impl.screen; + +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackCreator; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.world.CreateWorldScreen; +import net.minecraft.client.gui.screen.world.LevelScreenProvider; +import net.minecraft.client.gui.screen.world.WorldCreationSettings; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.world.GeneratorOptionsHolder; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.resource.ResourcePackManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.resource.VanillaDataPackProvider; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.server.SaveLoading; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Util; +import net.minecraft.world.gen.FlatLevelGeneratorPreset; +import net.minecraft.world.gen.GeneratorOptions; +import net.minecraft.world.gen.WorldPresets; +import net.minecraft.world.level.WorldGenSettings; +import xyz.nucleoid.plasmid.api.game.config.GameConfig; +import xyz.nucleoid.plasmid.client.impl.GameWorldCreator; + +import java.util.function.Function; + +public class GamesScreen extends Screen { + public static final Text TITLE = Text.translatable("text.plasmid.menu.games"); + public static final Text PREPARING_MESSAGE = Text.translatable("text.plasmid.menu.games.preparing"); + + private static final Text SEARCH_TEXT = Text.translatable("text.plasmid.menu.games.search"); + + private static final Text PLAY_BUTTON = Text.translatable("text.plasmid.menu.games.play"); + private static final Text CANCEL_BUTTON = ScreenTexts.CANCEL; + + protected final MinecraftClient client; + private final Screen parent; + private final GeneratorOptionsHolder generatorOptionsHolder; + + private TextFieldWidget searchBox; + private ButtonWidget playButton; + + private RegistryEntry> selectedGame; + + public GamesScreen(MinecraftClient client, Screen parent, GeneratorOptionsHolder generatorOptionsHolder) { + super(TITLE); + + this.client = client; + this.parent = parent; + this.generatorOptionsHolder = generatorOptionsHolder; + } + + public void setSelectedGame(RegistryEntry> selectedGame) { + this.selectedGame = selectedGame; + this.playButton.active = selectedGame != null; + } + + public void play() { + GameWorldCreator.create(this.client, this.generatorOptionsHolder, this.selectedGame); + } + + @Override + protected void init() { + var gameList = new GameListWidget(this.generatorOptionsHolder.getCombinedRegistryManager(), this); + + this.searchBox = new TextFieldWidget(this.textRenderer, (this.width - ButtonWidget.field_49479) / 2, 22, ButtonWidget.field_49479, ButtonWidget.DEFAULT_HEIGHT, this.searchBox, SEARCH_TEXT); + this.searchBox.setChangedListener(search -> gameList.setSearch(search)); + + this.playButton = ButtonWidget.builder(PLAY_BUTTON, button -> this.play()) + .position(this.width / 2 - 155, this.height - 28) + .build(); + + var cancelButton = ButtonWidget.builder(CANCEL_BUTTON, button -> this.close()) + .position(this.width / 2 + 5, this.height - 28) + .build(); + + this.addDrawableChild(this.searchBox); + this.addDrawableChild(gameList); + this.addDrawableChild(this.playButton); + this.addDrawableChild(cancelButton); + + this.setSelectedGame(this.selectedGame); + } + + @Override + protected void setInitialFocus() { + this.setInitialFocus(this.searchBox); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 8, Colors.WHITE); + } + + public static void show(MinecraftClient client, Screen parent) { + Function settingsSupplier = context -> new WorldGenSettings( + GeneratorOptions.createTestWorld(), + WorldPresets.createTestOptions(context.worldGenRegistryManager()) + ); + + CreateWorldScreen.showMessage(client, PREPARING_MESSAGE); + var packManager = new ResourcePackManager(new VanillaDataPackProvider(client.getSymlinkFinder())); + + // See https://github.com/FabricMC/fabric/blob/9ceeb58c7d15e9a5a9e311b0595d7908b8d5f3b3/fabric-resource-loader-v0/src/client/java/net/fabricmc/fabric/mixin/resource/loader/client/CreateWorldScreenMixin.java + packManager.providers.add(new ModResourcePackCreator(ResourceType.SERVER_DATA)); + var config = CreateWorldScreen.createServerConfig(packManager, ModResourcePackUtil.createDefaultDataConfiguration()); + + var future = SaveLoading.load( + config, + context -> new SaveLoading.LoadContext<>( + new WorldCreationSettings(settingsSupplier.apply(context), + context.dataConfiguration()), + context.dimensionsRegistryManager() + ), + (resourceManager, dataPackContents, registries, settings) -> { + resourceManager.close(); + + RegistryEntry preset = registries.getCombinedRegistryManager() + .getOrThrow(RegistryKeys.FLAT_LEVEL_GENERATOR_PRESET) + .getOrThrow(GameWorldCreator.OPTIONS.flatLevelPreset()); + + return new GeneratorOptionsHolder( + settings.worldGenSettings().generatorOptions(), + settings.worldGenSettings().dimensionOptionsRegistryHolder(), + registries, + dataPackContents, + settings.dataConfiguration(), + GameWorldCreator.OPTIONS + ).apply(LevelScreenProvider.createModifier(preset.value().settings())); + }, + Util.getMainWorkerExecutor(), + client + ); + + client.runTasks(future::isDone); + client.setScreen(new GamesScreen(client, parent, future.join())); + } + + @Override + public void close() { + this.client.setScreen(this.parent); + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java index 452f65cc..e2ba8990 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java @@ -27,6 +27,7 @@ import xyz.nucleoid.plasmid.impl.game.common.ui.WaitingLobbyUi; import xyz.nucleoid.plasmid.impl.game.common.ui.element.LeaveGameWaitingLobbyUiElement; import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; +import xyz.nucleoid.plasmid.impl.game.manager.HasForcedGameSpace; import xyz.nucleoid.plasmid.impl.compatibility.AfkDisplayCompatibility; import java.util.ArrayList; @@ -250,7 +251,9 @@ private void onRemovePlayer(ServerPlayerEntity player) { } private void onBuildUiLayout(WaitingLobbyUiLayout layout, ServerPlayerEntity player) { - layout.addTrailing(new LeaveGameWaitingLobbyUiElement(this.gameSpace, player)); + if (!HasForcedGameSpace.hasForcedGameSpace(this.gameSpace.getServer())) { + layout.addTrailing(new LeaveGameWaitingLobbyUiElement(this.gameSpace, player)); + } } private void updateCountdown() { diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/command/GameCommand.java b/src/main/java/xyz/nucleoid/plasmid/impl/command/GameCommand.java index 769c8399..28369b82 100644 --- a/src/main/java/xyz/nucleoid/plasmid/impl/command/GameCommand.java +++ b/src/main/java/xyz/nucleoid/plasmid/impl/command/GameCommand.java @@ -29,6 +29,7 @@ import xyz.nucleoid.plasmid.api.game.config.GameConfig; import xyz.nucleoid.plasmid.api.game.config.GameConfigs; import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; +import xyz.nucleoid.plasmid.impl.game.manager.HasForcedGameSpace; import xyz.nucleoid.plasmid.api.game.player.GamePlayerJoiner; import xyz.nucleoid.plasmid.api.game.player.JoinIntent; import xyz.nucleoid.plasmid.api.util.Scheduler; @@ -63,7 +64,7 @@ public static void register(CommandDispatcher dispatcher) { dispatcher.register( literal("game") .then(literal("open") - .requires(Permissions.require("plasmid.command.game.open", 2)) + .requires(Permissions.require("plasmid.command.game.open", 2).and(GameCommand::isNotForcedGameSpace)) .then(GameConfigArgument.argument("game_config") .executes(GameCommand::openGame) ) @@ -72,7 +73,7 @@ public static void register(CommandDispatcher dispatcher) { ) ) .then(literal("propose") - .requires(Permissions.require("plasmid.command.game.propose", 2)) + .requires(Permissions.require("plasmid.command.game.propose", 2).and(GameCommand::isNotForcedGameSpace)) .then(GameSpaceArgument.argument("game_space") .executes(GameCommand::proposeGame) ) @@ -90,36 +91,45 @@ public static void register(CommandDispatcher dispatcher) { ) ) .then(literal("kick") - .requires(Permissions.require("plasmid.command.game.kick", 2)) + .requires(Permissions.require("plasmid.command.game.kick", 2).and(GameCommand::isNotForcedGameSpace)) .then(argument("targets", EntityArgumentType.players()) .executes(GameCommand::kickPlayers) ) ) .then(literal("join") + .requires(Permissions.require("plasmid.command.game.joinall", 2).and(GameCommand::isNotForcedGameSpace)) .executes(ctx -> GameCommand.joinGame(ctx, JoinIntent.PLAY)) .then(GameSpaceArgument.argument("game_space") .executes(ctx -> GameCommand.joinQualifiedGame(ctx, JoinIntent.PLAY)) ) ) .then(literal("spectate") + .requires(Permissions.require("plasmid.command.game.joinall", 2).and(GameCommand::isNotForcedGameSpace)) .executes(ctx -> GameCommand.joinGame(ctx, JoinIntent.SPECTATE)) .then(GameSpaceArgument.argument("game_space") .executes(ctx -> GameCommand.joinQualifiedGame(ctx, JoinIntent.SPECTATE)) ) ) .then(literal("joinall") - .requires(Permissions.require("plasmid.command.game.joinall", 2)) + .requires(Permissions.require("plasmid.command.game.joinall", 2).and(GameCommand::isNotForcedGameSpace)) .executes(GameCommand::joinAllGame) .then(GameSpaceArgument.argument("game_space") .executes(GameCommand::joinAllQualifiedGame) ) ) .then(literal("locate") + .requires(GameCommand::isNotForcedGameSpace) .then(argument("player", EntityArgumentType.player()) .executes(GameCommand::locatePlayer)) ) - .then(literal("leave").executes(GameCommand::leaveGame)) - .then(literal("list").executes(GameCommand::listGames)) + .then(literal("leave") + .requires(GameCommand::isNotForcedGameSpace) + .executes(GameCommand::leaveGame) + ) + .then(literal("list") + .requires(GameCommand::isNotForcedGameSpace) + .executes(GameCommand::listGames) + ) ); } // @formatter:on @@ -451,4 +461,8 @@ private static int kickPlayers(CommandContext context) thro return successes; } + + protected static boolean isNotForcedGameSpace(ServerCommandSource source) { + return !HasForcedGameSpace.hasForcedGameSpace(source.getServer()); + } } diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/command/GamePortalCommand.java b/src/main/java/xyz/nucleoid/plasmid/impl/command/GamePortalCommand.java index eaa750d1..f0c9c8b6 100644 --- a/src/main/java/xyz/nucleoid/plasmid/impl/command/GamePortalCommand.java +++ b/src/main/java/xyz/nucleoid/plasmid/impl/command/GamePortalCommand.java @@ -31,6 +31,7 @@ public static void register(CommandDispatcher dispatcher) { dispatcher.register( literal("game") .then(literal("portal") + .requires(GameCommand::isNotForcedGameSpace) .then(literal("connect") .requires(Permissions.require("plasmid.command.game.portal.connect", 3)) .then(GamePortalArgument.argument("portal") diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/command/GameTestCommand.java b/src/main/java/xyz/nucleoid/plasmid/impl/command/GameTestCommand.java index 100fdfa9..34d7181c 100644 --- a/src/main/java/xyz/nucleoid/plasmid/impl/command/GameTestCommand.java +++ b/src/main/java/xyz/nucleoid/plasmid/impl/command/GameTestCommand.java @@ -17,7 +17,7 @@ public static void register(CommandDispatcher dispatcher) { dispatcher.register( literal("game") .then(literal("test") - .requires(Permissions.require("plasmid.command.game.test", 2)) + .requires(Permissions.require("plasmid.command.game.test", 2).and(GameCommand::isNotForcedGameSpace)) .then(GameConfigArgument.argument("game_config") .executes(GameTestCommand::openTestGame) ) diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/HasForcedGameSpace.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/HasForcedGameSpace.java new file mode 100644 index 00000000..ebcc337d --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/HasForcedGameSpace.java @@ -0,0 +1,14 @@ +package xyz.nucleoid.plasmid.impl.game.manager; + +import net.minecraft.server.MinecraftServer; +import xyz.nucleoid.plasmid.api.game.GameSpace; + +public interface HasForcedGameSpace { + GameSpace getForcedGameSpace(); + + void setForcedGameSpace(GameSpace gameSpace); + + static boolean hasForcedGameSpace(MinecraftServer server) { + return ((HasForcedGameSpace) server).getForcedGameSpace() != null; + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/player/isolation/IsolatingPlayerTeleporter.java b/src/main/java/xyz/nucleoid/plasmid/impl/player/isolation/IsolatingPlayerTeleporter.java index d0280cb7..6bceea60 100644 --- a/src/main/java/xyz/nucleoid/plasmid/impl/player/isolation/IsolatingPlayerTeleporter.java +++ b/src/main/java/xyz/nucleoid/plasmid/impl/player/isolation/IsolatingPlayerTeleporter.java @@ -6,8 +6,10 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; import net.minecraft.world.biome.source.BiomeAccess; import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.impl.game.manager.HasForcedGameSpace; import java.util.function.Function; @@ -21,6 +23,8 @@ * after teleporting and no weird issues can arise from invalid state passing through dimensions. */ public final class IsolatingPlayerTeleporter { + private static final Text GAME_ENDED_REASON = Text.translatable("text.plasmid.game.closed"); + private final MinecraftServer server; public IsolatingPlayerTeleporter(MinecraftServer server) { @@ -70,6 +74,11 @@ public void teleportOut(ServerPlayerEntity player) { } private void teleport(ServerPlayerEntity player, Function recreate, boolean in) { + if (!in && HasForcedGameSpace.hasForcedGameSpace(this.server)) { + player.networkHandler.disconnect(GAME_ENDED_REASON); + return; + } + var playerManager = this.server.getPlayerManager(); var playerManagerAccess = (PlayerManagerAccess) playerManager; diff --git a/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/MinecraftServerMixin.java b/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/MinecraftServerMixin.java new file mode 100644 index 00000000..6b442bd2 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/MinecraftServerMixin.java @@ -0,0 +1,23 @@ +package xyz.nucleoid.plasmid.mixin.game.space; + +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.impl.game.manager.HasForcedGameSpace; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin implements HasForcedGameSpace { + @Unique + private GameSpace forcedGameSpace = null; + + @Override + public GameSpace getForcedGameSpace() { + return this.forcedGameSpace; + } + + @Override + public void setForcedGameSpace(GameSpace gameSpace) { + this.forcedGameSpace = gameSpace; + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java b/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java index 62a995f2..b24b6712 100644 --- a/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java +++ b/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java @@ -8,10 +8,12 @@ import net.minecraft.entity.player.PlayerEntity; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtOps; +import net.minecraft.network.ClientConnection; import net.minecraft.network.packet.c2s.common.SyncedClientOptions; import net.minecraft.registry.RegistryKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ConnectedClientData; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.stat.ServerStatHandler; @@ -30,11 +32,14 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import xyz.nucleoid.plasmid.api.game.player.JoinIntent; import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; +import xyz.nucleoid.plasmid.impl.game.manager.HasForcedGameSpace; import xyz.nucleoid.plasmid.impl.player.isolation.PlayerManagerAccess; import xyz.nucleoid.plasmid.impl.player.isolation.PlayerResetter; import java.util.Map; +import java.util.Set; import java.util.UUID; @Mixin(PlayerManager.class) @@ -99,6 +104,15 @@ private void respawnPlayer( } } + @Inject(method = "onPlayerConnect", at = @At("TAIL")) + private void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, CallbackInfo ci) { + var gameSpace = ((HasForcedGameSpace) this.server).getForcedGameSpace(); + + if (gameSpace != null) { + gameSpace.getPlayers().offer(Set.of(player), JoinIntent.PLAY); + } + } + @Override public void plasmid$savePlayerData(ServerPlayerEntity player) { this.savePlayerData(player); diff --git a/src/main/resources/assets/plasmid/lang/en_us.json b/src/main/resources/assets/plasmid/lang/en_us.json new file mode 100644 index 00000000..8e7c8e27 --- /dev/null +++ b/src/main/resources/assets/plasmid/lang/en_us.json @@ -0,0 +1,6 @@ +{ + "text.plasmid.menu.games": "Games", + "text.plasmid.menu.games.play": "Play", + "text.plasmid.menu.games.preparing": "Preparing for game creation...", + "text.plasmid.menu.games.search": "Search for games" +} diff --git a/src/main/resources/data/plasmid/lang/en_us.json b/src/main/resources/data/plasmid/lang/en_us.json index 571939d3..37f383cf 100644 --- a/src/main/resources/data/plasmid/lang/en_us.json +++ b/src/main/resources/data/plasmid/lang/en_us.json @@ -23,6 +23,7 @@ "text.plasmid.entry": " - %s", "text.plasmid.game_config.game_config_does_not_exist": "Game config with id '%s' does not exist!", "text.plasmid.game_config.game_not_found": "Game config with id '%s' was not found!", + "text.plasmid.game.closed": "The game has closed!", "text.plasmid.game.portal.connect.block": "Connected '%1$s' to block at (%2$s; %3$s; %4$s)", "text.plasmid.game.portal.connect.entity": "Connected '%1$s' to '%2$s''", "text.plasmid.game.portal.connect.interface_already_connected": "The selected interface is already connected to this portal!", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index aa345ed9..eac28f24 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -8,7 +8,8 @@ "license": "LGPLv3", "environment": "*", "entrypoints": { - "main": ["xyz.nucleoid.plasmid.impl.Plasmid"] + "main": ["xyz.nucleoid.plasmid.impl.Plasmid"], + "client": ["xyz.nucleoid.plasmid.client.impl.PlasmidClient"] }, "mixins": [ "plasmid.mixins.json", diff --git a/src/main/resources/plasmid.accesswidener b/src/main/resources/plasmid.accesswidener index 6e158aa9..a15b1bcf 100644 --- a/src/main/resources/plasmid.accesswidener +++ b/src/main/resources/plasmid.accesswidener @@ -13,3 +13,8 @@ accessible method net/minecraft/entity/Entity unsetRemoved ()V accessible method net/minecraft/server/network/ServerPlayerInteractionManager setGameMode (Lnet/minecraft/world/GameMode;Lnet/minecraft/world/GameMode;)V accessible method net/minecraft/block/TntBlock primeTnt (Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/entity/LivingEntity;)V + +accessible method net/minecraft/client/gui/screen/world/CreateWorldScreen showMessage (Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/text/Text;)V +accessible method net/minecraft/client/gui/screen/world/CreateWorldScreen createSession (Lnet/minecraft/client/MinecraftClient;Ljava/lang/String;Ljava/nio/file/Path;)Ljava/util/Optional; +accessible method net/minecraft/client/gui/screen/world/CreateWorldScreen createServerConfig (Lnet/minecraft/resource/ResourcePackManager;Lnet/minecraft/resource/DataConfiguration;)Lnet/minecraft/server/SaveLoading$ServerConfig; +accessible field net/minecraft/resource/ResourcePackManager providers Ljava/util/Set; diff --git a/src/main/resources/plasmid.mixins.json b/src/main/resources/plasmid.mixins.json index 2090615b..0f0de976 100644 --- a/src/main/resources/plasmid.mixins.json +++ b/src/main/resources/plasmid.mixins.json @@ -19,6 +19,7 @@ "game.rule.ServerPlayNetworkHandlerMixin", "game.rule.TridentEntityMixin", "game.space.EntityMixin", + "game.space.MinecraftServerMixin", "game.space.PlayerManagerMixin", "game.space.ScreenHandlerMixin", "game.space.ServerPlayerEntityMixin"