diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java index 14fe4a280..6088e6791 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java @@ -3,17 +3,22 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.logging.LogUtils; -import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.*; +import net.earthcomputer.clientcommands.command.SnakeCommand; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.PlayerListEntry; import net.minecraft.network.encryption.PlayerPublicKey; import net.minecraft.network.encryption.PublicPlayerSession; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.slf4j.Logger; import java.security.PublicKey; +import java.util.List; +import java.util.Optional; public class CCNetworkHandler implements CCPacketListener { @@ -32,6 +37,16 @@ public static CCNetworkHandler getInstance() { return instance; } + public static Optional getPlayerByName(String name) { + assert MinecraftClient.getInstance().getNetworkHandler() != null; + return MinecraftClient.getInstance() + .getNetworkHandler() + .getPlayerList() + .stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(name)) + .findFirst(); + } + public void sendPacket(C2CPacket packet, PlayerListEntry recipient) throws CommandSyntaxException { Integer id = CCPacketHandler.getId(packet.getClass()); if (id == null) { @@ -63,6 +78,7 @@ public void sendPacket(C2CPacket packet, PlayerListEntry recipient) throws Comma if (commandString.length() >= 256) { throw MESSAGE_TOO_LONG_EXCEPTION.create(); } + assert MinecraftClient.getInstance().getNetworkHandler() != null; MinecraftClient.getInstance().getNetworkHandler().sendChatCommand(commandString); OutgoingPacketFilter.addPacket(packetString); } @@ -79,4 +95,76 @@ public void onMessageC2CPacket(MessageC2CPacket packet) { Text text = prefix.append(Text.translatable("ccpacket.messageC2CPacket.incoming", sender, message).formatted(Formatting.GRAY)); MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(text); } + + @Override + public void onSnakeInviteC2CPacket(SnakeInviteC2CPacket packet) { + // TODO: Use Text.translatable + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage( + Text.literal(packet.sender()) + .append(" invited you to a game of snake. ") + .append( + Text.literal("[Join]") + .styled(style -> + style.withColor(Formatting.GREEN) + .withHoverEvent(new HoverEvent( + HoverEvent.Action.SHOW_TEXT, Text.literal("Join game") + )) + .withClickEvent(new ClickEvent( + ClickEvent.Action.RUN_COMMAND, "/csnake join " + packet.sender() + )) + ) + ) + .styled(style -> style.withColor(Formatting.GRAY)) + ); + } + + @Override + public void onSnakeJoinC2CPacket(SnakeJoinC2CPacket packet) { + if (!(MinecraftClient.getInstance().currentScreen instanceof SnakeCommand.SnakeGameScreen snakeScreen)) return; + final SnakeAddPlayersC2CPacket addPlayersPacket = new SnakeAddPlayersC2CPacket(List.of(packet.sender())); + for (final String otherPlayer : snakeScreen.getOtherSnakes().keySet()) { + getPlayerByName(otherPlayer).ifPresent(actualPlayer -> { + try { + sendPacket(addPlayersPacket, actualPlayer); + } catch (CommandSyntaxException e) { + LOGGER.warn("Failed to recast snake player join", e); + } + }); + } + getPlayerByName(packet.sender()).ifPresent(sender -> { + try { + sendPacket(new SnakeAddPlayersC2CPacket(snakeScreen.getOtherSnakes().keySet()), sender); + sendPacket(new SnakeSyncAppleC2CPacket(snakeScreen.getApple()), sender); + } catch (CommandSyntaxException e) { + LOGGER.warn("Failed to reply to snake player join", e); + } + }); + snakeScreen.getOtherSnakes().put(packet.sender(), List.of()); // Reserve entry in player list + } + + @Override + public void onSnakeAddPlayersC2CPacket(SnakeAddPlayersC2CPacket packet) { + if (!(MinecraftClient.getInstance().currentScreen instanceof SnakeCommand.SnakeGameScreen snakeScreen)) return; + for (final String player : packet.players()) { + snakeScreen.getOtherSnakes().put(player, List.of()); + } + } + + @Override + public void onSnakeBodyC2CPacket(SnakeBodyC2CPacket packet) { + if (!(MinecraftClient.getInstance().currentScreen instanceof SnakeCommand.SnakeGameScreen snakeScreen)) return; + snakeScreen.getOtherSnakes().put(packet.sender(), packet.segments()); + } + + @Override + public void onSnakeRemovePlayerC2CPacket(SnakeRemovePlayerC2CPacket packet) { + if (!(MinecraftClient.getInstance().currentScreen instanceof SnakeCommand.SnakeGameScreen snakeScreen)) return; + snakeScreen.getOtherSnakes().remove(packet.player()); + } + + @Override + public void onSnakeSyncAppleC2CPacket(SnakeSyncAppleC2CPacket packet) { + if (!(MinecraftClient.getInstance().currentScreen instanceof SnakeCommand.SnakeGameScreen snakeScreen)) return; + snakeScreen.setApple(packet.applePos()); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java index f3903b6cf..5842330e8 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java @@ -2,7 +2,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.*; import net.minecraft.util.Util; import org.jetbrains.annotations.Nullable; @@ -17,6 +17,12 @@ public class CCPacketHandler { static { CCPacketHandler.register(MessageC2CPacket.class, MessageC2CPacket::new); + CCPacketHandler.register(SnakeInviteC2CPacket.class, SnakeInviteC2CPacket::new); + CCPacketHandler.register(SnakeJoinC2CPacket.class, SnakeJoinC2CPacket::new); + CCPacketHandler.register(SnakeAddPlayersC2CPacket.class, SnakeAddPlayersC2CPacket::new); + CCPacketHandler.register(SnakeBodyC2CPacket.class, SnakeBodyC2CPacket::new); + CCPacketHandler.register(SnakeRemovePlayerC2CPacket.class, SnakeRemovePlayerC2CPacket::new); + CCPacketHandler.register(SnakeSyncAppleC2CPacket.class, SnakeSyncAppleC2CPacket::new); } public static

void register(Class

packet, Function packetFactory) { diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketListener.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketListener.java index 734cb6e6c..4d52f6b5f 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketListener.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketListener.java @@ -1,7 +1,19 @@ package net.earthcomputer.clientcommands.c2c; -import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.*; public interface CCPacketListener { void onMessageC2CPacket(MessageC2CPacket packet); + + void onSnakeInviteC2CPacket(SnakeInviteC2CPacket packet); + + void onSnakeJoinC2CPacket(SnakeJoinC2CPacket packet); + + void onSnakeAddPlayersC2CPacket(SnakeAddPlayersC2CPacket packet); + + void onSnakeBodyC2CPacket(SnakeBodyC2CPacket packet); + + void onSnakeRemovePlayerC2CPacket(SnakeRemovePlayerC2CPacket packet); + + void onSnakeSyncAppleC2CPacket(SnakeSyncAppleC2CPacket packet); } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/StringBuf.java b/src/main/java/net/earthcomputer/clientcommands/c2c/StringBuf.java index 23909f489..b2d7c420d 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/StringBuf.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/StringBuf.java @@ -1,6 +1,10 @@ package net.earthcomputer.clientcommands.c2c; import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.IntFunction; public class StringBuf { @@ -35,6 +39,15 @@ public int readInt() { return Integer.parseInt(this.readString()); } + public > C readCollection(IntFunction collectionCreator, Function elementReader) { + final int count = readInt(); + final C result = collectionCreator.apply(count); + for (int i = 0; i < count; i++) { + result.add(elementReader.apply(this)); + } + return result; + } + public void writeString(String string) { this.buffer.append(string).append('\0'); } @@ -42,4 +55,11 @@ public void writeString(String string) { public void writeInt(int integer) { this.buffer.append(integer).append('\0'); } + + public void writeCollection(Collection collection, BiConsumer elementWriter) { + writeInt(collection.size()); + for (final E element : collection) { + elementWriter.accept(this, element); + } + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeAddPlayersC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeAddPlayersC2CPacket.java new file mode 100644 index 000000000..51861b11f --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeAddPlayersC2CPacket.java @@ -0,0 +1,24 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.StringBuf; + +import java.util.ArrayList; +import java.util.Collection; + +public record SnakeAddPlayersC2CPacket(Collection players) implements C2CPacket { + public SnakeAddPlayersC2CPacket(StringBuf buf) { + this((Collection)buf.readCollection(ArrayList::new, StringBuf::readString)); + } + + @Override + public void write(StringBuf buf) { + buf.writeCollection(players, StringBuf::writeString); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onSnakeAddPlayersC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeBodyC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeBodyC2CPacket.java new file mode 100644 index 000000000..8c449f9a0 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeBodyC2CPacket.java @@ -0,0 +1,26 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.StringBuf; +import net.earthcomputer.clientcommands.command.SnakeCommand; + +import java.util.ArrayList; +import java.util.List; + +public record SnakeBodyC2CPacket(String sender, List segments) implements C2CPacket { + public SnakeBodyC2CPacket(StringBuf buf) { + this(buf.readString(), buf.readCollection(ArrayList::new, SnakeCommand.Vec2i::new)); + } + + @Override + public void write(StringBuf buf) { + buf.writeString(sender); + buf.writeCollection(segments, SnakeCommand.Vec2i::write); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onSnakeBodyC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeInviteC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeInviteC2CPacket.java new file mode 100644 index 000000000..c80a29519 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeInviteC2CPacket.java @@ -0,0 +1,21 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.StringBuf; + +public record SnakeInviteC2CPacket(String sender) implements C2CPacket { + public SnakeInviteC2CPacket(StringBuf buf) { + this(buf.readString()); + } + + @Override + public void write(StringBuf buf) { + buf.writeString(sender); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onSnakeInviteC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeJoinC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeJoinC2CPacket.java new file mode 100644 index 000000000..a189d72ff --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeJoinC2CPacket.java @@ -0,0 +1,21 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.StringBuf; + +public record SnakeJoinC2CPacket(String sender) implements C2CPacket { + public SnakeJoinC2CPacket(StringBuf buf) { + this(buf.readString()); + } + + @Override + public void write(StringBuf buf) { + buf.writeString(sender); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onSnakeJoinC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeRemovePlayerC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeRemovePlayerC2CPacket.java new file mode 100644 index 000000000..4abd1a814 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeRemovePlayerC2CPacket.java @@ -0,0 +1,21 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.StringBuf; + +public record SnakeRemovePlayerC2CPacket(String player) implements C2CPacket { + public SnakeRemovePlayerC2CPacket(StringBuf buf) { + this(buf.readString()); + } + + @Override + public void write(StringBuf buf) { + buf.writeString(player); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onSnakeRemovePlayerC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeSyncAppleC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeSyncAppleC2CPacket.java new file mode 100644 index 000000000..1f66587a5 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/SnakeSyncAppleC2CPacket.java @@ -0,0 +1,22 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.StringBuf; +import net.earthcomputer.clientcommands.command.SnakeCommand; + +public record SnakeSyncAppleC2CPacket(SnakeCommand.Vec2i applePos) implements C2CPacket { + public SnakeSyncAppleC2CPacket(StringBuf buf) { + this(new SnakeCommand.Vec2i(buf)); + } + + @Override + public void write(StringBuf buf) { + SnakeCommand.Vec2i.write(buf, applePos); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onSnakeSyncAppleC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/SnakeCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/SnakeCommand.java index ce90df3bd..4f5067f1c 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/SnakeCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/SnakeCommand.java @@ -1,169 +1,348 @@ package net.earthcomputer.clientcommands.command; +import com.mojang.authlib.GameProfile; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.logging.LogUtils; +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCNetworkHandler; +import net.earthcomputer.clientcommands.c2c.StringBuf; +import net.earthcomputer.clientcommands.c2c.packets.*; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawableHelper; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.network.PlayerListEntry; import net.minecraft.client.render.GameRenderer; import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.sound.SoundEvents; import net.minecraft.text.MutableText; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; import net.minecraft.util.math.Direction; +import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; +import org.slf4j.Logger; -import java.util.LinkedList; -import java.util.ListIterator; -import java.util.Random; +import java.util.*; +import java.util.stream.Collectors; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; +import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.gameProfile; +import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.getCProfileArgument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class SnakeCommand { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final SimpleCommandExceptionType PLAYER_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.cwe.playerNotFound")); + public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("csnake") - .executes(ctx -> snake(ctx.getSource()))); + .executes(ctx -> snake(ctx.getSource(), null)) + .then(literal("invite") + .then(argument("player", gameProfile()) + .executes(ctx -> invite(ctx.getSource(), getCProfileArgument(ctx, "player"))) + ) + ) + .then(literal("join") + .then(argument("player", gameProfile()) + .executes(ctx -> joinGame(ctx.getSource(), getCProfileArgument(ctx, "player"))) + ) + ) + ); } - private static int snake(FabricClientCommandSource source) { + private static int joinGame(FabricClientCommandSource source, Collection profiles) throws CommandSyntaxException { + if (profiles.size() != 1) { + throw PLAYER_NOT_FOUND_EXCEPTION.create(); + } + assert source.getClient().getNetworkHandler() != null; + final PlayerListEntry gameToJoin = CCNetworkHandler.getPlayerByName(profiles.iterator().next().getName()) + .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); + return snake(source, gameToJoin); + } + + private static int snake(FabricClientCommandSource source, @Nullable PlayerListEntry gameToJoin) throws CommandSyntaxException { + final String otherSnake; + if (gameToJoin != null) { + otherSnake = gameToJoin.getProfile().getName(); + assert source.getClient().getNetworkHandler() != null; + CCNetworkHandler.getInstance().sendPacket(new SnakeJoinC2CPacket( + source.getClient().getNetworkHandler().getProfile().getName() + ), gameToJoin); + } else { + otherSnake = null; + } /* After executing a command, the current screen will be closed (the chat hud). And if you open a new screen in a command, that new screen will be closed instantly along with the chat hud. Slightly delaying the opening of the screen fixes this issue. */ - source.getClient().send(() -> source.getClient().setScreen(new SnakeGameScreen())); + source.getClient().send(() -> source.getClient().setScreen(new SnakeGameScreen(otherSnake))); return Command.SINGLE_SUCCESS; } -} -class SnakeGameScreen extends Screen { + private static int invite(FabricClientCommandSource source, Collection profiles) throws CommandSyntaxException { + assert source.getClient().getNetworkHandler() != null; + final String sender = source.getClient().getNetworkHandler().getProfile().getName(); + final Set names = profiles.stream() + .map(GameProfile::getName) + .collect(Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))); + try { + final long inviteCount = source.getClient().getNetworkHandler().getPlayerList().stream() + .filter(entry -> names.contains(entry.getProfile().getName())) + .peek(player -> { + final SnakeInviteC2CPacket packet = new SnakeInviteC2CPacket(sender); + try { + CCNetworkHandler.getInstance().sendPacket(packet, player); + } catch (CommandSyntaxException e) { + throw new RuntimeException(e); + } + // TODO: Text.translatable + source.sendFeedback( + Text.literal("Invited ") + .append(Objects.requireNonNullElseGet( + player.getDisplayName(), () -> Text.literal(player.getProfile().getName()) + )) + .append(" to a game of snake.") + .styled(style -> style.withColor(Formatting.GRAY)) + ); + }) + .count(); + if (inviteCount == 0L) { + source.sendFeedback(Text.literal("Couldn't find any players.")); + } + snake(source, null); + return (int)inviteCount; + } catch (RuntimeException e) { + if (e.getCause() instanceof CommandSyntaxException syntaxException) { + throw syntaxException; + } + throw e; + } + } - private static final MinecraftClient client = MinecraftClient.getInstance(); + public static class SnakeGameScreen extends Screen { - private static final Identifier GRID_TEXTURE = new Identifier("clientcommands:textures/snake_grid.png"); + private static final MinecraftClient client = MinecraftClient.getInstance(); - private static final Random random = new Random(); + private static final Identifier GRID_TEXTURE = new Identifier("clientcommands:textures/snake_grid.png"); - private static final int MAX_X = 16; - private static final int MAX_Z = 16; + private static final Random random = new Random(); - private int tickCounter = 10; - private Direction direction = Direction.EAST; - private Direction lastMoved = Direction.EAST; - private final LinkedList snake = new LinkedList<>(); - private Vec2i apple; + private static final int MAX_X = 24; + private static final int MAX_Z = 24; - SnakeGameScreen() { - super(Text.translatable("snakeGame.title")); - this.snake.add(new Vec2i(6, 8)); - this.snake.add(new Vec2i(5, 8)); - this.snake.add(new Vec2i(4, 8)); - do { - this.apple = new Vec2i(random.nextInt(MAX_X + 1), random.nextInt(MAX_Z + 1)); - } while (this.snake.contains(this.apple)); - } + private int tickCounter = 10; + private Direction direction = Direction.EAST; + private Direction lastMoved = Direction.EAST; + private final LinkedList snake = new LinkedList<>(); + private final Map> otherSnakes = new HashMap<>(); + private final Queue directionQueue = new ArrayDeque<>(); + private Vec2i apple; - @Override - public void tick() { - if (--this.tickCounter < 0) { - this.tickCounter = 3; - this.move(); + SnakeGameScreen(@Nullable String otherSnake) { + super(Text.translatable("snakeGame.title")); + if (otherSnake != null) { + otherSnakes.put(otherSnake, List.of()); + } + this.snake.add(new Vec2i(6, 8)); + this.snake.add(new Vec2i(5, 8)); + this.snake.add(new Vec2i(4, 8)); + do { + this.apple = new Vec2i(random.nextInt(MAX_X + 1), random.nextInt(MAX_Z + 1)); + } while (this.snake.contains(this.apple)); } - } - @Override - public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { - this.renderBackground(matrices); - super.render(matrices, mouseX, mouseY, delta); - } + @Override + public void tick() { + if (--this.tickCounter < 0) { + this.tickCounter = 3; + this.move(); + } + } - @Override - public void renderBackground(MatrixStack matrices) { - super.renderBackground(matrices); - int startX = (this.width - 289) / 2; - int startY = (this.height - 289) / 2; - - drawTextWithShadow(matrices, client.textRenderer, this.title, startX, startY - 10, 0xff_ffffff); - MutableText score = Text.translatable("snakeGame.score", this.snake.size()); - drawCenteredText(matrices, client.textRenderer, score, this.width / 2, startY - 10, 0xff_ffffff); - - RenderSystem.setShader(GameRenderer::getPositionTexProgram); - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - RenderSystem.setShaderTexture(0, GRID_TEXTURE); - drawTexture(matrices, startX, startY, 0, 0, 289, 289, 289, 289); - int scaleX = MAX_X + 1; - int scaleZ = MAX_Z + 1; - DrawableHelper.fill(matrices, startX + this.apple.x() * scaleX, startY + this.apple.z() * scaleZ, startX + this.apple.x() * scaleX + scaleX, startY + this.apple.z() * scaleZ + scaleZ, 0xff_f52559); - for (Vec2i vec : this.snake) { - DrawableHelper.fill(matrices, startX + vec.x() * scaleX, startY + vec.z() * scaleZ, startX + vec.x() * scaleX + scaleX, startY + vec.z() * scaleZ + scaleZ, 0xff_1f2df6); + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + super.render(matrices, mouseX, mouseY, delta); } - } - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (client.options.forwardKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_UP) { - return this.setDirection(Direction.NORTH); - } else if (client.options.leftKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_LEFT) { - return this.setDirection(Direction.WEST); - } else if (client.options.backKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_DOWN) { - return this.setDirection(Direction.SOUTH); - } else if (client.options.rightKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_RIGHT) { - return this.setDirection(Direction.EAST); - } - return super.keyPressed(keyCode, scanCode, modifiers); - } + @Override + public void renderBackground(MatrixStack matrices) { + super.renderBackground(matrices); + int startX = (this.width - 425) / 2; + int startY = (this.height - 425) / 2; + + drawTextWithShadow(matrices, client.textRenderer, this.title, startX, startY - 10, 0xff_ffffff); + MutableText score = Text.translatable("snakeGame.score", this.snake.size()); + drawCenteredText(matrices, client.textRenderer, score, this.width / 2, startY - 10, 0xff_ffffff); - private void move() { - Vec2i head = this.snake.getFirst(); - this.snake.addFirst(new Vec2i(head.x() + this.direction.getOffsetX(), head.z() + this.direction.getOffsetZ())); - this.lastMoved = this.direction; - if (this.checkGameOver()) { - client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.ENTITY_OCELOT_DEATH, 1)); - this.close(); - return; + RenderSystem.setShader(GameRenderer::getPositionTexProgram); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.setShaderTexture(0, GRID_TEXTURE); + drawTexture(matrices, startX, startY, 0, 0, 425, 425, 425, 425); + int scaleX = 17; + int scaleZ = 17; + for (final Map.Entry> otherSnake : otherSnakes.entrySet()) { + if (otherSnake.getKey().equals(client.getSession().getUsername())) continue; + for (final Vec2i vec : otherSnake.getValue()) { + DrawableHelper.fill(matrices, startX + vec.x() * scaleX, startY + vec.z() * scaleZ, startX + vec.x() * scaleX + scaleX, startY + vec.z() * scaleZ + scaleZ, 0xffffa500); + } + } + DrawableHelper.fill(matrices, startX + this.apple.x() * scaleX, startY + this.apple.z() * scaleZ, startX + this.apple.x() * scaleX + scaleX, startY + this.apple.z() * scaleZ + scaleZ, 0xff_f52559); + for (Vec2i vec : this.snake) { + DrawableHelper.fill(matrices, startX + vec.x() * scaleX, startY + vec.z() * scaleZ, startX + vec.x() * scaleX + scaleX, startY + vec.z() * scaleZ + scaleZ, 0xff_1f2df6); + } + for (final Map.Entry> otherSnake : otherSnakes.entrySet()) { + if (otherSnake.getKey().equals(client.getSession().getUsername())) continue; + if (!otherSnake.getValue().isEmpty()) { + final Vec2i head = otherSnake.getValue().get(0); + DrawableHelper.drawCenteredText( + matrices, textRenderer, + otherSnake.getKey(), + startX + head.x() * scaleX + scaleX / 2, + startY + head.z() * scaleZ - scaleZ, + 0xffffffff + ); + } + } } - this.checkApple(); - } - private boolean checkGameOver() { - Vec2i head = this.snake.getFirst(); - if (head.x() < 0 || head.x() > MAX_X || head.z() < 0 || head.z() > MAX_Z) { - return true; + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (client.options.forwardKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_UP) { + return enqueueMove(Direction.NORTH); + } else if (client.options.leftKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_LEFT) { + return enqueueMove(Direction.WEST); + } else if (client.options.backKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_DOWN) { + return enqueueMove(Direction.SOUTH); + } else if (client.options.rightKey.matchesKey(keyCode, scanCode) || keyCode == GLFW.GLFW_KEY_RIGHT) { + return enqueueMove(Direction.EAST); + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + private void move() { + while (!directionQueue.isEmpty()) { + if (setDirection(directionQueue.remove())) break; + } + Vec2i head = this.snake.getFirst(); + this.snake.addFirst(new Vec2i(head.x() + this.direction.getOffsetX(), head.z() + this.direction.getOffsetZ())); + this.lastMoved = this.direction; + if (this.checkGameOver()) { + client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.ENTITY_OCELOT_DEATH, 1)); + this.close(); + return; + } + this.checkApple(); + syncPlayerData(); + } + + @Override + public void close() { + super.close(); + if (otherSnakes.isEmpty()) return; + assert client.getNetworkHandler() != null; + broadcastPacket(new SnakeRemovePlayerC2CPacket(client.getNetworkHandler().getProfile().getName())); } - ListIterator it = this.snake.listIterator(1); - while (it.hasNext()) { - if (it.next().equals(head)) { + + private boolean checkGameOver() { + Vec2i head = this.snake.getFirst(); + if (head.x() < 0 || head.x() > MAX_X || head.z() < 0 || head.z() > MAX_Z) { return true; } + ListIterator it = this.snake.listIterator(1); + while (it.hasNext()) { + if (it.next().equals(head)) { + return true; + } + } + return false; } - return false; - } - private void checkApple() { - Vec2i head = this.snake.getFirst(); - if (head.equals(this.apple)) { - client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.ENTITY_GENERIC_EAT, 1)); - do { - this.apple = new Vec2i(random.nextInt(MAX_X + 1), random.nextInt(MAX_Z + 1)); - } while (this.snake.contains(this.apple)); - } else { - this.snake.removeLast(); + private void checkApple() { + Vec2i head = this.snake.getFirst(); + if (head.equals(this.apple)) { + client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.ENTITY_GENERIC_EAT, 1)); + do { + this.apple = new Vec2i(random.nextInt(MAX_X + 1), random.nextInt(MAX_Z + 1)); + } while (anyContainsPos(this.apple)); + broadcastPacket(new SnakeSyncAppleC2CPacket(apple)); + } else { + this.snake.removeLast(); + } } - } - private boolean setDirection(Direction direction) { - if (this.lastMoved == direction.getOpposite()) { + private boolean anyContainsPos(Vec2i pos) { + if (snake.contains(pos)) return true; + for (final List otherSnake : otherSnakes.values()) { + if (otherSnake.contains(pos)) return true; + } return false; } - this.direction = direction; - return true; + + private void syncPlayerData() { + if (otherSnakes.isEmpty()) return; + assert client.getNetworkHandler() != null; + broadcastPacket(new SnakeBodyC2CPacket(client.getNetworkHandler().getProfile().getName(), snake)); + } + + private void broadcastPacket(C2CPacket packet) { + for (final String otherSnake : otherSnakes.keySet()) { + CCNetworkHandler.getPlayerByName(otherSnake).ifPresent(player -> { + try { + CCNetworkHandler.getInstance().sendPacket(packet, player); + } catch (CommandSyntaxException e) { + LOGGER.warn("Failed to broadcast packet to " + otherSnake, e); + } + }); + } + } + + private boolean setDirection(Direction direction) { + if (this.lastMoved == direction.getOpposite() || this.lastMoved == direction) { + return false; + } + this.direction = direction; + return true; + } + + private boolean enqueueMove(Direction direction) { + while (directionQueue.size() > 2) { + directionQueue.remove(); + } + directionQueue.add(direction); + return true; + } + + public Map> getOtherSnakes() { + return otherSnakes; + } + + public Vec2i getApple() { + return apple; + } + + public void setApple(Vec2i apple) { + this.apple = apple; + } } -} -record Vec2i(int x, int z) { + public record Vec2i(int x, int z) { + public Vec2i(StringBuf buf) { + this(buf.readInt(), buf.readInt()); + } + + public static void write(StringBuf buf, Vec2i vec) { + buf.writeInt(vec.x); + buf.writeInt(vec.z); + } + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java index 5218063c0..26a8f2489 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java @@ -15,9 +15,12 @@ import java.util.Collection; -import static com.mojang.brigadier.arguments.StringArgumentType.*; -import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.*; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.gameProfile; +import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.getCProfileArgument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class WhisperEncryptedCommand { @@ -35,9 +38,7 @@ private static int whisper(FabricClientCommandSource source, Collection p.getProfile().getName().equalsIgnoreCase(profiles.iterator().next().getName())) - .findFirst() + PlayerListEntry recipient = CCNetworkHandler.getPlayerByName(profiles.iterator().next().getName()) .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); MessageC2CPacket packet = new MessageC2CPacket(source.getClient().getNetworkHandler().getProfile().getName(), message); diff --git a/src/main/resources/assets/clientcommands/textures/snake_grid.png b/src/main/resources/assets/clientcommands/textures/snake_grid.png index 52014adc0..43ab2249b 100644 Binary files a/src/main/resources/assets/clientcommands/textures/snake_grid.png and b/src/main/resources/assets/clientcommands/textures/snake_grid.png differ