diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 7e2f896fc..1ce694cbd 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -126,6 +126,7 @@ public static void registerCommands(CommandDispatcher CGameModeCommand.register(dispatcher); CGiveCommand.register(dispatcher, context); ChorusCommand.register(dispatcher); + ConnectFourCommand.register(dispatcher); CParticleCommand.register(dispatcher, context); CPlaySoundCommand.register(dispatcher); CrackRNGCommand.register(dispatcher); diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CFriendlyByteBuf.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CFriendlyByteBuf.java index 85231ead4..13b740e45 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CFriendlyByteBuf.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CFriendlyByteBuf.java @@ -4,15 +4,23 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.network.RegistryFriendlyByteBuf; +import java.util.UUID; + public class C2CFriendlyByteBuf extends RegistryFriendlyByteBuf { private final String sender; + private final UUID senderUUID; - public C2CFriendlyByteBuf(ByteBuf source, RegistryAccess registryAccess, String sender) { + public C2CFriendlyByteBuf(ByteBuf source, RegistryAccess registryAccess, String sender, UUID senderUUID) { super(source, registryAccess); this.sender = sender; + this.senderUUID = senderUUID; } public String getSender() { return this.sender; } + + public UUID getSenderUUID() { + return this.senderUUID; + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java index 50f1e774d..6d314028c 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java @@ -7,9 +7,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.PutConnectFourPieceC2CPacket; import net.earthcomputer.clientcommands.c2c.packets.PutTicTacToeMarkC2CPacket; -import net.earthcomputer.clientcommands.c2c.packets.StartTicTacToeGameC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.StartTwoPlayerGameC2CPacket; +import net.earthcomputer.clientcommands.command.ConnectFourCommand; import net.earthcomputer.clientcommands.command.ListenCommand; +import net.earthcomputer.clientcommands.features.TwoPlayerGame; import net.earthcomputer.clientcommands.command.TicTacToeCommand; import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.minecraft.ChatFormatting; @@ -36,6 +39,7 @@ import java.security.PublicKey; import java.util.Arrays; import java.util.Optional; +import java.util.UUID; public class C2CPacketHandler implements C2CPacketListener { private static final Logger LOGGER = LogUtils.getLogger(); @@ -46,8 +50,9 @@ public class C2CPacketHandler implements C2CPacketListener { public static final ProtocolInfo C2C = ProtocolInfoBuilder.clientboundProtocol(ConnectionProtocol.PLAY, builder -> builder .addPacket(MessageC2CPacket.ID, MessageC2CPacket.CODEC) - .addPacket(StartTicTacToeGameC2CPacket.ID, StartTicTacToeGameC2CPacket.CODEC) + .addPacket(StartTwoPlayerGameC2CPacket.ID, StartTwoPlayerGameC2CPacket.CODEC) .addPacket(PutTicTacToeMarkC2CPacket.ID, PutTicTacToeMarkC2CPacket.CODEC) + .addPacket(PutConnectFourPieceC2CPacket.ID, PutConnectFourPieceC2CPacket.CODEC) ).bind(b -> (C2CFriendlyByteBuf) b); public static final String C2C_PACKET_HEADER = "CCΕNC:"; @@ -72,7 +77,7 @@ public void sendPacket(Packet packet, PlayerInfo recipient) t throw PUBLIC_KEY_NOT_FOUND_EXCEPTION.create(); } PublicKey key = ppk.data().key(); - FriendlyByteBuf buf = wrapByteBuf(PacketByteBufs.create(), null); + FriendlyByteBuf buf = wrapByteBuf(PacketByteBufs.create(), null, null); if (buf == null) { return; } @@ -115,7 +120,7 @@ public void sendPacket(Packet packet, PlayerInfo recipient) t OutgoingPacketFilter.addPacket(packetString); } - public static boolean handleC2CPacket(String content, String sender) { + public static boolean handleC2CPacket(String content, String sender, UUID senderUUID) { byte[] encrypted = ConversionHelper.BaseUTF8.fromUnicode(content); // round down to multiple of 256 bytes int length = encrypted.length & ~0xFF; @@ -152,7 +157,7 @@ public static boolean handleC2CPacket(String content, String sender) { if (uncompressed == null) { return false; } - C2CFriendlyByteBuf buf = wrapByteBuf(Unpooled.wrappedBuffer(uncompressed), sender); + C2CFriendlyByteBuf buf = wrapByteBuf(Unpooled.wrappedBuffer(uncompressed), sender, senderUUID); if (buf == null) { return false; } @@ -195,8 +200,8 @@ public void onMessageC2CPacket(MessageC2CPacket packet) { } @Override - public void onStartTicTacToeGameC2CPacket(StartTicTacToeGameC2CPacket packet) { - TicTacToeCommand.onStartTicTacToeGameC2CPacket(packet); + public void onStartTwoPlayerGameC2CPacket(StartTwoPlayerGameC2CPacket packet) { + TwoPlayerGame.onStartTwoPlayerGame(packet); } @Override @@ -204,12 +209,17 @@ public void onPutTicTacToeMarkC2CPacket(PutTicTacToeMarkC2CPacket packet) { TicTacToeCommand.onPutTicTacToeMarkC2CPacket(packet); } - public static @Nullable C2CFriendlyByteBuf wrapByteBuf(ByteBuf buf, String sender) { + @Override + public void onPutConnectFourPieceC2CPacket(PutConnectFourPieceC2CPacket packet) { + ConnectFourCommand.onPutConnectFourPieceC2CPacket(packet); + } + + public static @Nullable C2CFriendlyByteBuf wrapByteBuf(ByteBuf buf, String sender, UUID senderUUID) { ClientPacketListener connection = Minecraft.getInstance().getConnection(); if (connection == null) { return null; } - return new C2CFriendlyByteBuf(buf, connection.registryAccess(), sender); + return new C2CFriendlyByteBuf(buf, connection.registryAccess(), sender, senderUUID); } @Override diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java index c4cea645c..6ed723016 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java @@ -1,14 +1,17 @@ package net.earthcomputer.clientcommands.c2c; import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.PutConnectFourPieceC2CPacket; import net.earthcomputer.clientcommands.c2c.packets.PutTicTacToeMarkC2CPacket; -import net.earthcomputer.clientcommands.c2c.packets.StartTicTacToeGameC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.StartTwoPlayerGameC2CPacket; import net.minecraft.network.ClientboundPacketListener; public interface C2CPacketListener extends ClientboundPacketListener { void onMessageC2CPacket(MessageC2CPacket packet); - void onStartTicTacToeGameC2CPacket(StartTicTacToeGameC2CPacket packet); + void onStartTwoPlayerGameC2CPacket(StartTwoPlayerGameC2CPacket packet); void onPutTicTacToeMarkC2CPacket(PutTicTacToeMarkC2CPacket packet); + + void onPutConnectFourPieceC2CPacket(PutConnectFourPieceC2CPacket packet); } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutConnectFourPieceC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutConnectFourPieceC2CPacket.java new file mode 100644 index 000000000..6517c78ff --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutConnectFourPieceC2CPacket.java @@ -0,0 +1,37 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CFriendlyByteBuf; +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.C2CPacketListener; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.PacketType; +import net.minecraft.resources.ResourceLocation; + +import java.util.UUID; + +public record PutConnectFourPieceC2CPacket(String sender, UUID senderUUID, int x) implements C2CPacket { + public static final StreamCodec CODEC = Packet.codec(PutConnectFourPieceC2CPacket::write, PutConnectFourPieceC2CPacket::new); + public static final PacketType ID = new PacketType<>(PacketFlow.CLIENTBOUND, ResourceLocation.fromNamespaceAndPath("clientcommands", "put_connect_four_piece")); + + public PutConnectFourPieceC2CPacket(C2CFriendlyByteBuf buf) { + this(buf.getSender(), buf.getSenderUUID(), buf.readVarInt()); + } + + public void write(C2CFriendlyByteBuf buf) { + buf.writeVarInt(this.x); + } + + @Override + public PacketType> type() { + return ID; + } + + @Override + public void handle(C2CPacketListener handler) { + handler.onPutConnectFourPieceC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java index 1bfd115fa..e52197bda 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java @@ -9,12 +9,14 @@ import net.minecraft.network.protocol.PacketType; import net.minecraft.resources.ResourceLocation; -public record PutTicTacToeMarkC2CPacket(String sender, byte x, byte y) implements C2CPacket { +import java.util.UUID; + +public record PutTicTacToeMarkC2CPacket(String sender, UUID senderUUID, byte x, byte y) implements C2CPacket { public static final StreamCodec CODEC = Packet.codec(PutTicTacToeMarkC2CPacket::write, PutTicTacToeMarkC2CPacket::new); public static final PacketType ID = new PacketType<>(PacketFlow.CLIENTBOUND, ResourceLocation.fromNamespaceAndPath("clientcommands", "put_tic_tac_toe_mark")); public PutTicTacToeMarkC2CPacket(C2CFriendlyByteBuf buf) { - this(buf.getSender(), buf.readByte(), buf.readByte()); + this(buf.getSender(), buf.getSenderUUID(), buf.readByte(), buf.readByte()); } public void write(C2CFriendlyByteBuf buf) { diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTicTacToeGameC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTicTacToeGameC2CPacket.java deleted file mode 100644 index 87f96c8d0..000000000 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTicTacToeGameC2CPacket.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.earthcomputer.clientcommands.c2c.packets; - -import net.earthcomputer.clientcommands.c2c.C2CPacket; -import net.earthcomputer.clientcommands.c2c.C2CPacketListener; -import net.earthcomputer.clientcommands.c2c.C2CFriendlyByteBuf; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.PacketFlow; -import net.minecraft.network.protocol.PacketType; -import net.minecraft.resources.ResourceLocation; - -public record StartTicTacToeGameC2CPacket(String sender, boolean accept) implements C2CPacket { - public static final StreamCodec CODEC = Packet.codec(StartTicTacToeGameC2CPacket::write, StartTicTacToeGameC2CPacket::new); - public static final PacketType ID = new PacketType<>(PacketFlow.CLIENTBOUND, ResourceLocation.fromNamespaceAndPath("clientcommands", "start_tic_tac_toe_game")); - - public StartTicTacToeGameC2CPacket(C2CFriendlyByteBuf buf) { - this(buf.getSender(), buf.readBoolean()); - } - - public void write(C2CFriendlyByteBuf buf) { - buf.writeBoolean(this.accept); - } - - @Override - public void handle(C2CPacketListener handler) { - handler.onStartTicTacToeGameC2CPacket(this); - } - - @Override - public PacketType> type() { - return ID; - } -} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTwoPlayerGameC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTwoPlayerGameC2CPacket.java new file mode 100644 index 000000000..8b5cc6c9d --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTwoPlayerGameC2CPacket.java @@ -0,0 +1,39 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CFriendlyByteBuf; +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.C2CPacketListener; +import net.earthcomputer.clientcommands.features.TwoPlayerGame; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.PacketType; +import net.minecraft.resources.ResourceLocation; + +import java.util.UUID; + +public record StartTwoPlayerGameC2CPacket(String sender, UUID senderUUID, boolean accept, TwoPlayerGame game) implements C2CPacket { + public static final StreamCodec CODEC = Packet.codec(StartTwoPlayerGameC2CPacket::write, StartTwoPlayerGameC2CPacket::new); + public static final PacketType ID = new PacketType<>(PacketFlow.CLIENTBOUND, ResourceLocation.fromNamespaceAndPath("clientcommands", "start_two_player_game")); + + public StartTwoPlayerGameC2CPacket(C2CFriendlyByteBuf buf) { + this(buf.getSender(), buf.getSenderUUID(), buf.readBoolean(), TwoPlayerGame.getById(buf.readResourceLocation())); + } + + public void write(C2CFriendlyByteBuf buf) { + buf.writeBoolean(this.accept); + buf.writeResourceLocation(this.game.getId()); + } + + @Override + public void handle(C2CPacketListener handler) { + handler.onStartTwoPlayerGameC2CPacket(this); + } + + @Override + public PacketType> type() { + return ID; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ConnectFourCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/ConnectFourCommand.java new file mode 100644 index 000000000..a7019b768 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/ConnectFourCommand.java @@ -0,0 +1,381 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.logging.LogUtils; +import net.earthcomputer.clientcommands.c2c.C2CPacketHandler; +import net.earthcomputer.clientcommands.c2c.packets.PutConnectFourPieceC2CPacket; +import net.earthcomputer.clientcommands.features.TwoPlayerGame; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.UUID; + +public class ConnectFourCommand { + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(TwoPlayerGame.CONNECT_FOUR_GAME_TYPE.createCommandTree()); + } + + public static void onPutConnectFourPieceC2CPacket(PutConnectFourPieceC2CPacket packet) { + UUID senderUUID = packet.senderUUID(); + ConnectFourGame game = TwoPlayerGame.CONNECT_FOUR_GAME_TYPE.getActiveGame(senderUUID); + if (game == null) { + return; + } + game.onMove(packet.x(), game.opponentPiece()); + } + + public static class ConnectFourGame { + public static final int WIDTH = 7; + public static final int HEIGHT = 6; + + public final PlayerInfo opponent; + public final Piece yourPiece; + public Piece activePiece; + public final Piece[][] board; + @Nullable + public Winner winner; + + public ConnectFourGame(PlayerInfo opponent, Piece yourPiece) { + this.opponent = opponent; + this.yourPiece = yourPiece; + this.activePiece = Piece.RED; + this.board = new Piece[WIDTH][HEIGHT]; + this.winner = null; + } + + public void onMove(int x, Piece piece) { + final Minecraft mc = Minecraft.getInstance(); + final ClientPacketListener connection = mc.getConnection(); + assert connection != null; + if (piece != activePiece) { + LOGGER.warn("Invalid piece, the active piece is {} and the piece that was attempted to be placed was {}", this.activePiece.translate(), piece.translate()); + return; + } + + if (!this.isGameActive()) { + LOGGER.warn("Tried to add piece to the already completed game with {}.", this.opponent.getProfile().getName()); + return; + } + + if (!this.addPiece(x, piece)) { + LOGGER.warn("Failed to add piece to your Connect Four game with {}.", this.opponent.getProfile().getName()); + return; + } + + if (this.isYourTurn()) { + try { + PutConnectFourPieceC2CPacket packet = new PutConnectFourPieceC2CPacket(connection.getLocalGameProfile().getName(), connection.getLocalGameProfile().getId(), x); + C2CPacketHandler.getInstance().sendPacket(packet, this.opponent); + } catch (CommandSyntaxException e) { + ClientCommandHelper.sendFeedback(Component.translationArg(e.getRawMessage())); + } + } + + String sender = this.opponent.getProfile().getName(); + UUID senderUUID = this.opponent.getProfile().getId(); + this.activePiece = piece.opposite(); + if ((this.winner = this.getWinner()) != null) { + if (this.winner == this.yourPiece.asWinner()) { + TwoPlayerGame.CONNECT_FOUR_GAME_TYPE.onWon(sender, senderUUID); + } else if (this.winner == this.yourPiece.opposite().asWinner()) { + TwoPlayerGame.CONNECT_FOUR_GAME_TYPE.onLost(sender, senderUUID); + } else if (this.winner == Winner.DRAW) { + TwoPlayerGame.CONNECT_FOUR_GAME_TYPE.onDraw(sender, senderUUID); + } + } else { + if (this.isYourTurn()) { + TwoPlayerGame.CONNECT_FOUR_GAME_TYPE.onMove(sender); + } + } + } + + public boolean isYourTurn() { + return this.activePiece == this.yourPiece; + } + + public boolean isGameActive() { + return this.winner == null; + } + + public boolean canMove() { + return this.isYourTurn() && this.isGameActive(); + } + + public Piece opponentPiece() { + return yourPiece.opposite(); + } + + public boolean addPiece(int x, Piece piece) { + int y; + if (isValidRow(x) && (y = this.getPlacementY(x)) < HEIGHT) { + this.board[x][y] = piece; + return true; + } + return false; + } + + @Nullable + private Winner getWinner() { + // check horizontally + for (int x = 0; x < WIDTH - 3; x++) { + for (int y = 0; y < HEIGHT; y++) { + if (this.board[x][y] != null && this.board[x][y] == this.board[x + 1][y] && this.board[x][y] == this.board[x + 2][y] && this.board[x][y] == this.board[x + 3][y]) { + return this.board[x][y].asWinner(); + } + } + } + + // check vertically + for (int x = 0; x < WIDTH; x++) { + for (int y = 0; y < HEIGHT - 3; y++) { + if (this.board[x][y] != null && this.board[x][y] == this.board[x][y + 1] && this.board[x][y] == this.board[x][y + 2] && this.board[x][y] == this.board[x][y + 3]) { + return this.board[x][y].asWinner(); + } + } + } + + // check horizontally (northeast) + for (int x = 0; x < WIDTH - 3; x++) { + for (int y = 0; y < HEIGHT - 3; y++) { + if (this.board[x][y] != null && this.board[x][y] == this.board[x + 1][y + 1] && this.board[x][y] == this.board[x + 2][y + 2] && this.board[x][y] == this.board[x + 3][y + 3]) { + return this.board[x][y].asWinner(); + } + } + } + + // check horizontally (southeast) + for (int x = 0; x < WIDTH - 3; x++) { + for (int y = 3; y < HEIGHT; y++) { + if (this.board[x][y] != null && this.board[x][y] == this.board[x + 1][y - 1] && this.board[x][y] == this.board[x + 2][y - 2] && this.board[x][y] == this.board[x + 3][y - 3]) { + return this.board[x][y].asWinner(); + } + } + } + + for (int x = 0; x < WIDTH; x++) { + for (int y = 0; y < HEIGHT; y++) { + if (this.board[x][y] == null) { + // still a space to play + return null; + } + } + } + + // no spaces left, game ends in a draw + return Winner.DRAW; + } + + public static boolean isValidRow(int x) { + return 0 <= x && x < WIDTH; + } + + public int getPlacementY(int x) { + int y = 0; + for (Piece piece : this.board[x]) { + if (piece == null) { + break; + } + y++; + } + + return y; + } + } + + public enum Piece { + RED, + YELLOW; + + public Piece opposite() { + return switch (this) { + case RED -> YELLOW; + case YELLOW -> RED; + }; + } + + public Component translate() { + return switch (this) { + case RED -> Component.translatable("connectFourGame.pieceRed"); + case YELLOW -> Component.translatable("connectFourGame.pieceYellow"); + }; + } + + public Winner asWinner() { + return switch (this) { + case RED -> Winner.RED; + case YELLOW -> Winner.YELLOW; + }; + } + + public void render(GuiGraphics graphics, int x, int y, boolean transparent) { + int xOffset = switch (this) { + case RED -> 0; + case YELLOW -> 16; + }; + graphics.blit( + RenderType::guiTextured, + ConnectFourGameScreen.PIECES_TEXTURE, + x, + y, + xOffset, + 0, + ConnectFourGameScreen.PIECE_WIDTH, + ConnectFourGameScreen.PIECE_HEIGHT, + ConnectFourGameScreen.TEXTURE_PIECE_WIDTH, + ConnectFourGameScreen.TEXTURE_PIECE_HEIGHT, + ConnectFourGameScreen.TEXTURE_PIECES_WIDTH, + ConnectFourGameScreen.TEXTURE_PIECES_HEIGHT, + transparent ? 0X7F_FFFFFF : 0XFF_FFFFFF + ); + } + } + + public enum Winner { + RED, + YELLOW, + DRAW + } + + public static class ConnectFourGameScreen extends Screen { + private final ConnectFourGame game; + + private static final ResourceLocation BOARD_TEXTURE = ResourceLocation.fromNamespaceAndPath("clientcommands", "textures/connect_four/board.png"); + private static final ResourceLocation PIECES_TEXTURE = ResourceLocation.fromNamespaceAndPath("clientcommands", "textures/connect_four/pieces.png"); + + private static final int SCALE = 4; + + private static final int TEXTURE_PIECE_WIDTH = 16; + private static final int TEXTURE_PIECE_HEIGHT = 16; + private static final int TEXTURE_BOARD_BORDER_WIDTH = 1; + private static final int TEXTURE_BOARD_BORDER_HEIGHT = 1; + private static final int TEXTURE_SLOT_BORDER_WIDTH = 1; + private static final int TEXTURE_SLOT_BORDER_HEIGHT = 1; + private static final int TEXTURE_SLOT_WIDTH = TEXTURE_PIECE_WIDTH + 2 * TEXTURE_SLOT_BORDER_WIDTH; + private static final int TEXTURE_SLOT_HEIGHT = TEXTURE_PIECE_HEIGHT + 2 * TEXTURE_SLOT_BORDER_HEIGHT; + private static final int TEXTURE_BOARD_WIDTH = TEXTURE_SLOT_WIDTH * ConnectFourGame.WIDTH + TEXTURE_BOARD_BORDER_WIDTH * 2; + private static final int TEXTURE_BOARD_HEIGHT = TEXTURE_SLOT_HEIGHT * ConnectFourGame.HEIGHT + TEXTURE_BOARD_BORDER_HEIGHT * 2; + private static final int TEXTURE_PIECES_WIDTH = 2 * TEXTURE_PIECE_WIDTH; // red and yellow + private static final int TEXTURE_PIECES_HEIGHT = TEXTURE_PIECE_HEIGHT; + + private static final int BOARD_WIDTH = SCALE * TEXTURE_BOARD_WIDTH; + private static final int BOARD_HEIGHT = SCALE * TEXTURE_BOARD_HEIGHT; + private static final int PIECE_WIDTH = SCALE * TEXTURE_PIECE_WIDTH; + private static final int PIECE_HEIGHT = SCALE * TEXTURE_PIECE_HEIGHT; + private static final int BOARD_BORDER_WIDTH = SCALE * TEXTURE_BOARD_BORDER_WIDTH; + private static final int BOARD_BORDER_HEIGHT = SCALE * TEXTURE_BOARD_BORDER_HEIGHT; + private static final int SLOT_BORDER_WIDTH = SCALE * TEXTURE_SLOT_BORDER_WIDTH; + private static final int SLOT_BORDER_HEIGHT = SCALE * TEXTURE_SLOT_BORDER_HEIGHT; + private static final int SLOT_WIDTH = SCALE * TEXTURE_SLOT_WIDTH; + private static final int SLOT_HEIGHT = SCALE * TEXTURE_SLOT_HEIGHT; + + public ConnectFourGameScreen(ConnectFourGame game) { + super(Component.translatable("connectFourGame.title", game.opponent.getProfile().getName())); + this.game = game; + } + + @Override + public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.renderBackground(graphics, mouseX, mouseY, partialTick); + int startX = (this.width - BOARD_WIDTH) / 2; + int startY = (this.height - BOARD_HEIGHT) / 2; + Component gameStateTranslate = getGameStateTranslate(); + + graphics.drawString(this.font, Component.translatable("connectFourGame.pieceSet", this.game.yourPiece.translate()), startX, startY - 20, 0xff_ffffff); + graphics.drawString(this.font, this.title, startX, startY - 10, 0xff_ffffff); + graphics.drawString(this.font, gameStateTranslate, startX + BOARD_WIDTH - this.font.width(gameStateTranslate), startY - 10, 0xff_ffffff); + + graphics.blit( + RenderType::guiTextured, + BOARD_TEXTURE, + startX, + startY, + 0, + 0, + BOARD_WIDTH, + BOARD_HEIGHT, + TEXTURE_BOARD_WIDTH, + TEXTURE_BOARD_HEIGHT, + TEXTURE_BOARD_WIDTH, + TEXTURE_BOARD_HEIGHT + ); + + for (int x = 0; x < ConnectFourGame.WIDTH; x++) { + for (int y = 0; y < ConnectFourGame.HEIGHT; y++) { + Piece piece = this.game.board[x][y]; + if (piece != null) { + piece.render(graphics, startX + BOARD_BORDER_WIDTH + SLOT_WIDTH * x + SLOT_BORDER_WIDTH, startY + BOARD_BORDER_HEIGHT + SLOT_HEIGHT * (ConnectFourGame.HEIGHT - 1 - y) + SLOT_BORDER_HEIGHT, false); + } + } + } + + int boardMinX = startX + BOARD_BORDER_WIDTH; + int boardMaxX = startX + BOARD_WIDTH - BOARD_BORDER_WIDTH * 2; + int boardMaxY = startY + BOARD_HEIGHT; + if (this.game.canMove() && boardMinX <= mouseX && mouseX < boardMaxX && mouseY < boardMaxY) { + int x = (mouseX - boardMinX) / SLOT_WIDTH; + int y = this.game.getPlacementY(x); + if (y < ConnectFourGame.HEIGHT) { + game.yourPiece.render(graphics, startX + BOARD_BORDER_WIDTH + SLOT_WIDTH * x + SLOT_BORDER_WIDTH, startY + BOARD_BORDER_HEIGHT + SLOT_HEIGHT * (ConnectFourGame.HEIGHT - 1 - y) + SLOT_BORDER_HEIGHT, true); + } + } + } + + private Component getGameStateTranslate() { + if (game.isGameActive()) { + if (this.game.isYourTurn()) { + return Component.translatable("connectFourGame.yourMove"); + } else { + return Component.translatable("connectFourGame.opponentMove"); + } + } else { + if (game.winner == Winner.DRAW) { + return Component.translatable("connectFourGame.draw"); + } else if (game.winner == game.yourPiece.asWinner()) { + return Component.translatable("connectFourGame.won").withStyle(ChatFormatting.GREEN); + } else { + return Component.translatable("connectFourGame.lost").withStyle(ChatFormatting.RED); + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int startX = (this.width - BOARD_WIDTH) / 2; + int startY = (this.height - BOARD_HEIGHT) / 2; + + int boardMinX = startX + BOARD_BORDER_WIDTH; + int boardMaxX = startX + BOARD_WIDTH - BOARD_BORDER_WIDTH * 2; + int boardMaxY = startY + BOARD_HEIGHT; + + if (!(boardMinX <= mouseX && mouseX < boardMaxX && mouseY < boardMaxY)) { + return super.mouseClicked(mouseX, mouseY, button); + } + + if (button != InputConstants.MOUSE_BUTTON_LEFT) { + return false; + } + + int x = (int) ((mouseX - boardMinX) / SLOT_WIDTH); + if (this.game.canMove()) { + this.game.onMove(x, game.yourPiece); + return true; + } + + return false; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java index 8ac3043fa..b17d6d939 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java @@ -1,143 +1,48 @@ package net.earthcomputer.clientcommands.command; -import com.google.common.cache.CacheBuilder; -import com.mojang.authlib.GameProfile; import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import net.earthcomputer.clientcommands.c2c.C2CPacketHandler; import net.earthcomputer.clientcommands.c2c.packets.PutTicTacToeMarkC2CPacket; -import net.earthcomputer.clientcommands.c2c.packets.StartTicTacToeGameC2CPacket; +import net.earthcomputer.clientcommands.features.TwoPlayerGame; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.client.renderer.RenderType; -import net.minecraft.commands.SharedSuggestionProvider; -import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.HoverEvent; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; -import java.time.Duration; -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -import static com.mojang.brigadier.arguments.StringArgumentType.*; -import static dev.xpple.clientarguments.arguments.CGameProfileArgument.*; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; +import java.util.UUID; public class TicTacToeCommand { - - private static final SimpleCommandExceptionType PLAYER_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.ctictactoe.playerNotFound")); - private static final SimpleCommandExceptionType NO_GAME_WITH_PLAYER_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.ctictactoe.noGameWithPlayer")); - - private static final Map activeGames = CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(15)).build().asMap(); - private static final Set pendingInvites = Collections.newSetFromMap(CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(5)).build().asMap()); - public static void register(CommandDispatcher dispatcher) { - dispatcher.register(literal("ctictactoe") - .then(literal("start") - .then(argument("opponent", gameProfile(true)) - .executes(ctx -> start(ctx.getSource(), getSingleProfileArgument(ctx, "opponent"))))) - .then(literal("open") - .then(argument("opponent", word()) - .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(activeGames.keySet(), builder)) - .executes(ctx -> open(ctx.getSource(), getString(ctx, "opponent")))))); - } - - private static int start(FabricClientCommandSource source, GameProfile player) throws CommandSyntaxException { - PlayerInfo recipient = source.getClient().getConnection().getPlayerInfo(player.getId()); - if (recipient == null) { - throw PLAYER_NOT_FOUND_EXCEPTION.create(); - } - - StartTicTacToeGameC2CPacket packet = new StartTicTacToeGameC2CPacket(source.getClient().getConnection().getLocalGameProfile().getName(), false); - C2CPacketHandler.getInstance().sendPacket(packet, recipient); - pendingInvites.add(recipient.getProfile().getName()); - source.sendFeedback(Component.translatable("c2cpacket.startTicTacToeGameC2CPacket.outgoing.invited", recipient.getProfile().getName())); - return Command.SINGLE_SUCCESS; - } - - public static void onStartTicTacToeGameC2CPacket(StartTicTacToeGameC2CPacket packet) { - String sender = packet.sender(); - PlayerInfo opponent = Minecraft.getInstance().getConnection().getPlayerInfo(sender); - if (opponent == null) { - return; - } - - if (packet.accept() && pendingInvites.remove(sender)) { - TicTacToeGame game = new TicTacToeGame(opponent, TicTacToeGame.Mark.CROSS); - activeGames.put(sender, game); - - MutableComponent component = Component.translatable("c2cpacket.startTicTacToeGameC2CPacket.incoming.accepted", sender); - component.withStyle(style -> style - .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/ctictactoe open " + sender)) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("/ctictactoe open " + sender)))); - Minecraft.getInstance().gui.getChat().addMessage(component); - return; - } - - MutableComponent component = Component.translatable("c2cpacket.startTicTacToeGameC2CPacket.incoming", sender); - component - .append(" [") - .append(Component.translatable("c2cpacket.startTicTacToeGameC2CPacket.incoming.accept").withStyle(style -> style - .withColor(ChatFormatting.GREEN) - .withClickEvent(new ClickEvent(ClickEvent.Action.CHANGE_PAGE, ClientCommandHelper.registerCode(() -> { - TicTacToeGame game = new TicTacToeGame(opponent, TicTacToeGame.Mark.NOUGHT); - activeGames.put(sender, game); - - StartTicTacToeGameC2CPacket acceptPacket = new StartTicTacToeGameC2CPacket(Minecraft.getInstance().getConnection().getLocalGameProfile().getName(), true); - try { - C2CPacketHandler.getInstance().sendPacket(acceptPacket, opponent); - } catch (CommandSyntaxException e) { - Minecraft.getInstance().gui.getChat().addMessage(Component.translationArg(e.getRawMessage())); - } - - Minecraft.getInstance().gui.getChat().addMessage(Component.translatable("c2cpacket.startTicTacToeGameC2CPacket.outgoing.accept")); - }))) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("c2cpacket.startTicTacToeGameC2CPacket.incoming.accept.hover"))))) - .append("]"); - Minecraft.getInstance().gui.getChat().addMessage(component); - } - - private static int open(FabricClientCommandSource source, String name) throws CommandSyntaxException { - TicTacToeGame game = activeGames.get(name); - if (game == null) { - throw NO_GAME_WITH_PLAYER_EXCEPTION.create(); - } - - source.getClient().schedule(() -> source.getClient().setScreen(new TicTacToeGameScreen(game))); - return Command.SINGLE_SUCCESS; + dispatcher.register(TwoPlayerGame.TIC_TAC_TOE_GAME_TYPE.createCommandTree()); } public static void onPutTicTacToeMarkC2CPacket(PutTicTacToeMarkC2CPacket packet) { String sender = packet.sender(); - TicTacToeGame game = activeGames.get(sender); + UUID senderUUID = packet.senderUUID(); + TicTacToeGame game = TwoPlayerGame.TIC_TAC_TOE_GAME_TYPE.getActiveGame(senderUUID); if (game == null) { return; } if (game.putMark(packet.x(), packet.y(), game.yourMarks.opposite())) { - if (game.getWinner() == game.yourMarks.opposite()) { - Minecraft.getInstance().gui.getChat().addMessage(Component.translatable("c2cpacket.putTicTacToeMarkC2CPacket.incoming.lost", sender)); - activeGames.remove(sender); - return; + TicTacToeGame.Mark winner = game.getWinner(); + if (winner == game.yourMarks.opposite()) { + TwoPlayerGame.TIC_TAC_TOE_GAME_TYPE.onLost(sender, senderUUID); + } else if (game.isDrawn()) { + TwoPlayerGame.TIC_TAC_TOE_GAME_TYPE.onDraw(sender, senderUUID); + } else { + TwoPlayerGame.TIC_TAC_TOE_GAME_TYPE.onMove(sender); } - MutableComponent component = Component.translatable("c2cpacket.putTicTacToeMarkC2CPacket.incoming", sender); - component.withStyle(style -> style - .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/ctictactoe open " + sender)) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("/ctictactoe open " + sender)))); - Minecraft.getInstance().gui.getChat().addMessage(component); } } - private static class TicTacToeGame { + public static class TicTacToeGame { public final PlayerInfo opponent; private final Mark[][] board = new Mark[3][3]; @@ -161,6 +66,7 @@ public boolean putMark(byte x, byte y, Mark mark) { return false; } + @Nullable public Mark getWinner() { for (byte x = 0; x < 3; x++) { if (this.board[x][0] == this.board[x][1] && this.board[x][1] == this.board[x][2] && this.board[x][0] != null) { @@ -179,7 +85,19 @@ public Mark getWinner() { return null; } - private enum Mark { + public boolean isDrawn() { + for (Mark[] marks : this.board) { + for (Mark mark : marks) { + if (mark != null) { + return false; + } + } + } + + return true; + } + + public enum Mark { NOUGHT(Component.translatable("ticTacToeGame.noughts")), CROSS(Component.translatable("ticTacToeGame.crosses")); @@ -195,7 +113,7 @@ public Mark opposite() { } } - private static class TicTacToeGameScreen extends Screen { + public static class TicTacToeGameScreen extends Screen { private final TicTacToeGame game; private static final ResourceLocation GRID_TEXTURE = ResourceLocation.fromNamespaceAndPath("clientcommands", "textures/tic_tac_toe/grid.png"); @@ -210,7 +128,7 @@ private static class TicTacToeGameScreen extends Screen { private static final int MARK_SIZE = 76; private static final int PADDING = 2; - private TicTacToeGameScreen(TicTacToeGame game) { + public TicTacToeGameScreen(TicTacToeGame game) { super(Component.translatable("ticTacToeGame.title", game.opponent.getProfile().getName())); this.game = game; } @@ -224,7 +142,7 @@ public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, fl guiGraphics.drawString(this.font, this.title, startX, startY - 20, 0xff_ffffff); guiGraphics.drawString(this.font, Component.translatable("ticTacToeGame.playingWith", this.game.yourMarks.name), startX, startY - 10, 0xff_ffffff); - guiGraphics.blit(RenderType::guiTextured, GRID_TEXTURE, startX, startY, 0, 0, GRID_SIZE, GRID_SIZE, GRID_SIZE_TEXTURE, GRID_SIZE_TEXTURE); + guiGraphics.blit(RenderType::guiTextured, GRID_TEXTURE, startX, startY, 0, 0, GRID_SIZE, GRID_SIZE, GRID_SIZE_TEXTURE, GRID_SIZE_TEXTURE, GRID_SIZE_TEXTURE, GRID_SIZE_TEXTURE); TicTacToeGame.Mark[][] board = this.game.board; for (byte x = 0; x < 3; x++) { @@ -237,7 +155,7 @@ public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, fl case NOUGHT -> 0; case CROSS -> MARK_SIZE_TEXTURE; }; - guiGraphics.blit(RenderType::guiTextured, MARKS_TEXTURE, startX + (CELL_SIZE + BORDER_SIZE) * x + PADDING, startY + (CELL_SIZE + BORDER_SIZE) * y + PADDING, MARK_SIZE, MARK_SIZE, offset, 0, MARK_SIZE_TEXTURE, MARK_SIZE_TEXTURE, 2 * MARK_SIZE_TEXTURE, MARK_SIZE_TEXTURE); + guiGraphics.blit(RenderType::guiTextured, MARKS_TEXTURE, startX + (CELL_SIZE + BORDER_SIZE) * x + PADDING, startY + (CELL_SIZE + BORDER_SIZE) * y + PADDING, offset, 0, MARK_SIZE, MARK_SIZE, MARK_SIZE_TEXTURE, MARK_SIZE_TEXTURE, 2 * MARK_SIZE_TEXTURE, MARK_SIZE_TEXTURE); } } } @@ -265,13 +183,13 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { if (this.game.putMark(x, y, this.game.yourMarks)) { try { - PutTicTacToeMarkC2CPacket packet = new PutTicTacToeMarkC2CPacket(Minecraft.getInstance().getConnection().getLocalGameProfile().getName(), x, y); + PutTicTacToeMarkC2CPacket packet = new PutTicTacToeMarkC2CPacket(Minecraft.getInstance().getConnection().getLocalGameProfile().getName(), Minecraft.getInstance().getConnection().getLocalGameProfile().getId(), x, y); C2CPacketHandler.getInstance().sendPacket(packet, this.game.opponent); } catch (CommandSyntaxException e) { - Minecraft.getInstance().gui.getChat().addMessage(Component.translationArg(e.getRawMessage())); + ClientCommandHelper.sendFeedback(Component.translationArg(e.getRawMessage())); } if (this.game.getWinner() == this.game.yourMarks) { - activeGames.remove(this.game.opponent.getProfile().getName()); + TwoPlayerGame.TIC_TAC_TOE_GAME_TYPE.onWon(this.game.opponent.getProfile().getName(), this.game.opponent.getProfile().getId()); } return true; } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/TwoPlayerGame.java b/src/main/java/net/earthcomputer/clientcommands/features/TwoPlayerGame.java new file mode 100644 index 000000000..01dfef1c8 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/TwoPlayerGame.java @@ -0,0 +1,252 @@ +package net.earthcomputer.clientcommands.features; + +import com.demonwav.mcdev.annotations.Translatable; +import com.google.common.cache.CacheBuilder; +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.earthcomputer.clientcommands.c2c.C2CPacketHandler; +import net.earthcomputer.clientcommands.c2c.packets.StartTwoPlayerGameC2CPacket; +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.earthcomputer.clientcommands.command.ConnectFourCommand; +import net.earthcomputer.clientcommands.command.TicTacToeCommand; +import net.earthcomputer.clientcommands.event.ClientConnectionEvents; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + +import static com.mojang.brigadier.arguments.StringArgumentType.*; +import static dev.xpple.clientarguments.arguments.CGameProfileArgument.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class TwoPlayerGame { + public static final Map> TYPE_BY_NAME = new LinkedHashMap<>(); + private static final SimpleCommandExceptionType PLAYER_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("twoPlayerGame.playerNotFound")); + private static final SimpleCommandExceptionType NO_GAME_WITH_PLAYER_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("twoPlayerGame.noGameWithPlayer")); + + public static final TwoPlayerGame TIC_TAC_TOE_GAME_TYPE = register(new TwoPlayerGame<>("commands.ctictactoe.name", "ctictactoe", ResourceLocation.fromNamespaceAndPath("clientcommands", "tictactoe"), (opponent, firstPlayer) -> new TicTacToeCommand.TicTacToeGame(opponent, firstPlayer ? TicTacToeCommand.TicTacToeGame.Mark.CROSS : TicTacToeCommand.TicTacToeGame.Mark.NOUGHT), TicTacToeCommand.TicTacToeGameScreen::new)); + public static final TwoPlayerGame CONNECT_FOUR_GAME_TYPE = register(new TwoPlayerGame<>("commands.cconnectfour.name", "cconnectfour", ResourceLocation.fromNamespaceAndPath("clientcommands", "connectfour"), (opponent, firstPlayer) -> new ConnectFourCommand.ConnectFourGame(opponent, firstPlayer ? ConnectFourCommand.Piece.RED : ConnectFourCommand.Piece.YELLOW), ConnectFourCommand.ConnectFourGameScreen::new)); + + private static TwoPlayerGame register(TwoPlayerGame instance) { + TYPE_BY_NAME.put(instance.id, instance); + return instance; + } + + @Nullable + public static TwoPlayerGame getById(ResourceLocation id) { + return TYPE_BY_NAME.get(id); + } + + public static void onPlayerLeave(UUID opponentUUID) { + for (TwoPlayerGame game : TYPE_BY_NAME.values()) { + game.activeGames.remove(opponentUUID); + game.pendingInvites.remove(opponentUUID); + } + } + + static { + ClientConnectionEvents.DISCONNECT.register(() -> { + for (TwoPlayerGame game : TYPE_BY_NAME.values()) { + game.activeGames.clear(); + game.pendingInvites.clear(); + } + }); + } + + private final Component translation; + private final String command; + private final ResourceLocation id; + private final Set pendingInvites; + private final Map activeGames; + private final GameFactory gameFactory; + private final ScreenFactory screenFactory; + + TwoPlayerGame(@Translatable String translationKey, String command, ResourceLocation id, GameFactory gameFactory, ScreenFactory screenFactory) { + this.translation = Component.translatable(translationKey); + this.command = command; + this.id = id; + this.pendingInvites = Collections.newSetFromMap(CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(5)).build().asMap()); + this.activeGames = CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(15)).build().asMap(); + this.gameFactory = gameFactory; + this.screenFactory = screenFactory; + } + + public Component translate() { + return this.translation; + } + + public ResourceLocation getId() { + return this.id; + } + + public Set getPendingInvites() { + return this.pendingInvites; + } + + public Map getActiveGames() { + return this.activeGames; + } + + @Nullable + public T getActiveGame(UUID opponent) { + return this.activeGames.get(opponent); + } + + public void removeActiveGame(UUID opponent) { + this.activeGames.remove(opponent); + } + + public void addNewGame(PlayerInfo opponent, boolean isFirstPlayer) { + this.activeGames.put(opponent.getProfile().getId(), this.gameFactory.create(opponent, isFirstPlayer)); + } + + public LiteralArgumentBuilder createCommandTree() { + final Minecraft mc = Minecraft.getInstance(); + final ClientPacketListener connection = mc.getConnection(); + assert connection != null; + return literal(this.command) + .then(literal("start") + .then(argument("opponent", gameProfile(true)) + .executes(ctx -> this.start(ctx.getSource(), getSingleProfileArgument(ctx, "opponent"))))) + .then(literal("open") + .then(argument("opponent", word()) + .suggests((ctx, builder) -> SharedSuggestionProvider.suggest(this.getActiveGames().keySet().stream().flatMap(uuid -> Stream.ofNullable(connection.getPlayerInfo(uuid))).map(info -> info.getProfile().getName()), builder)) + .executes(ctx -> this.open(ctx.getSource(), getString(ctx, "opponent"))))); + } + + public int start(FabricClientCommandSource source, GameProfile player) throws CommandSyntaxException { + PlayerInfo recipient = source.getClient().getConnection().getPlayerInfo(player.getId()); + if (recipient == null) { + throw PLAYER_NOT_FOUND_EXCEPTION.create(); + } + + StartTwoPlayerGameC2CPacket packet = new StartTwoPlayerGameC2CPacket(player.getName(), player.getId(), false, this); + C2CPacketHandler.getInstance().sendPacket(packet, recipient); + this.pendingInvites.add(player.getId()); + this.activeGames.remove(player.getId()); + source.sendFeedback(Component.translatable("c2cpacket.startTwoPlayerGameC2CPacket.outgoing.invited", player.getName(), translate())); + return Command.SINGLE_SUCCESS; + } + + public int open(FabricClientCommandSource source, String name) throws CommandSyntaxException { + PlayerInfo opponent = source.getClient().getConnection().getPlayerInfo(name); + if (opponent == null) { + throw PLAYER_NOT_FOUND_EXCEPTION.create(); + } + if (!openGame(opponent.getProfile().getId())) { + throw NO_GAME_WITH_PLAYER_EXCEPTION.create(); + } + + return Command.SINGLE_SUCCESS; + } + + private boolean openGame(UUID opponentUuid) { + final Minecraft mc = Minecraft.getInstance(); + T game = activeGames.get(opponentUuid); + if (game != null) { + mc.schedule(() -> mc.setScreen(this.screenFactory.createScreen(game))); + return true; + } else { + return false; + } + } + + public static void onStartTwoPlayerGame(StartTwoPlayerGameC2CPacket packet) { + final Minecraft mc = Minecraft.getInstance(); + String sender = packet.sender(); + TwoPlayerGame game = packet.game(); + PlayerInfo opponent = Minecraft.getInstance().getConnection().getPlayerInfo(sender); + if (opponent == null) { + return; + } + + if (packet.accept() && game.getPendingInvites().remove(opponent.getProfile().getId())) { + packet.game().addNewGame(opponent, true); + + MutableComponent clickable = Component.translatable("twoPlayerGame.clickToMakeYourMove"); + clickable.withStyle(style -> style + .withUnderlined(true) + .withColor(ChatFormatting.GREEN) + .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/" + game.command + " open " + sender)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("/" + game.command + " open " + sender)))); + ClientCommandHelper.sendFeedback(Component.translatable("c2cpacket.startTwoPlayerGameC2CPacket.incoming.accepted", sender, game.translate()).append(" [").append(clickable).append("]")); + } else { + game.getActiveGames().remove(opponent.getProfile().getId()); + MutableComponent clickable = Component.translatable("c2cpacket.startTwoPlayerGameC2CPacket.incoming.accept").withStyle(style -> + style + .withUnderlined(true) + .withColor(ChatFormatting.GREEN) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("c2cpacket.startTwoPlayerGameC2CPacket.incoming.accept.hover"))) + .withClickEvent(new ClickEvent(ClickEvent.Action.CHANGE_PAGE, ClientCommandHelper.registerCode(() -> { + if (!game.openGame(opponent.getProfile().getId())) { + game.addNewGame(opponent, false); + + StartTwoPlayerGameC2CPacket acceptPacket = new StartTwoPlayerGameC2CPacket(mc.getGameProfile().getName(), mc.getGameProfile().getId(), true, game); + try { + C2CPacketHandler.getInstance().sendPacket(acceptPacket, opponent); + } catch (CommandSyntaxException e) { + ClientCommandHelper.sendFeedback(Component.translationArg(e.getRawMessage())); + } + + ClientCommandHelper.sendFeedback("c2cpacket.startTwoPlayerGameC2CPacket.outgoing.accept"); + } + })))); + ClientCommandHelper.sendFeedback(Component.translatable("c2cpacket.startTwoPlayerGameC2CPacket.incoming", sender, game.translate()).append(" [").append(clickable).append("]")); + } + } + + public void onWon(String sender, UUID senderUUID) { + ClientCommandHelper.sendFeedback("twoPlayerGame.chat.won", translate(), sender); + removeActiveGame(senderUUID); + } + + public void onDraw(String sender, UUID senderUUID) { + ClientCommandHelper.sendFeedback("twoPlayerGame.chat.draw", translate(), sender); + removeActiveGame(senderUUID); + } + + public void onLost(String sender, UUID senderUUID) { + ClientCommandHelper.sendFeedback("twoPlayerGame.chat.lost", sender, translate()); + removeActiveGame(senderUUID); + } + + public void onMove(String sender) { + MutableComponent clickable = Component.translatable("twoPlayerGame.clickToMakeYourMove"); + clickable.withStyle(style -> style + .withColor(ChatFormatting.GREEN) + .withUnderlined(true) + .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/" + command + " open " + sender)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("/" + command + " open " + sender)))); + ClientCommandHelper.sendFeedback(Component.translatable("twoPlayerGame.incoming", sender, translate()).append(" [").append(clickable).append("]")); + } + + @FunctionalInterface + public interface GameFactory { + T create(PlayerInfo opponent, boolean isFirstPlayer); + } + + @FunctionalInterface + public interface ScreenFactory { + S createScreen(T t); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatListenerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatListenerMixin.java index 7d27b11e8..7acb774c6 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatListenerMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatListenerMixin.java @@ -45,7 +45,7 @@ private void onC2CPacket(ChatType.Bound boundChatType, PlayerChatMessage chatMes cir.setReturnValue(false); return; } - if (C2CPacketHandler.handleC2CPacket(packetString, gameProfile.getName())) { + if (C2CPacketHandler.handleC2CPacket(packetString, gameProfile.getName(), gameProfile.getId())) { cir.setReturnValue(true); } else { this.minecraft.gui.getChat().addMessage(Component.translatable("c2cpacket.malformedPacket").withStyle(ChatFormatting.RED)); diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ClientPacketListenerMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ClientPacketListenerMixin.java new file mode 100644 index 000000000..e56fab879 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ClientPacketListenerMixin.java @@ -0,0 +1,19 @@ +package net.earthcomputer.clientcommands.mixin.c2c; + +import com.llamalad7.mixinextras.sugar.Local; +import net.earthcomputer.clientcommands.features.TwoPlayerGame; +import net.minecraft.client.multiplayer.ClientPacketListener; +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 java.util.UUID; + +@Mixin(ClientPacketListener.class) +public class ClientPacketListenerMixin { + @Inject(method = "handlePlayerInfoRemove", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getPlayerSocialManager()Lnet/minecraft/client/gui/screens/social/PlayerSocialManager;")) + private void onHandlePlayerInfoRemove(CallbackInfo ci, @Local UUID uuid) { + TwoPlayerGame.onPlayerLeave(uuid); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/events/MinecraftMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/events/MinecraftMixin.java index a4b632707..ad2b3ffea 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/events/MinecraftMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/events/MinecraftMixin.java @@ -36,8 +36,8 @@ public void onOpenScreen(@Nullable Screen screen, CallbackInfo ci) { } } - @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("RETURN")) - public void onDisconnect(Screen screen, CallbackInfo ci) { + @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;Z)V", at = @At("RETURN")) + public void onDisconnect(CallbackInfo ci) { ClientConnectionEvents.DISCONNECT.invoker().onDisconnect(); } } diff --git a/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java b/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java index 7576c3869..bf14fd9ff 100644 --- a/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java +++ b/src/main/java/net/earthcomputer/clientcommands/util/DebugRandom.java @@ -19,8 +19,22 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; -import javax.swing.*; -import java.awt.*; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.UIManager; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index b032b098e..67fc071c3 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -5,16 +5,14 @@ "c2cpacket.messageC2CPacket.outgoing": "you -> %s: %s", "c2cpacket.messageTooLong": "Message too long (max. 255 characters) got %s characters", "c2cpacket.publicKeyNotFound": "Public key not found", - "c2cpacket.putTicTacToeMarkC2CPacket.incoming": "%s has made a move in tic-tac-toe, click to make your move", - "c2cpacket.putTicTacToeMarkC2CPacket.incoming.lost": "%s has made a move in tic-tac-toe and won", "c2cpacket.receivedC2CPacket": "You have received a C2C packet, but you aren't accepting incoming C2C packets! Hover to view the raw packet.", "c2cpacket.sentC2CPacket": "You have sent a C2C packet, but you aren't accepting incoming C2C packets!", - "c2cpacket.startTicTacToeGameC2CPacket.incoming": "%s invited you to a game of tic-tac-toe", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accept": "Accept", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accept.hover": "Click to accept", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accepted": "%s has accepted your invitation, click to make your move", - "c2cpacket.startTicTacToeGameC2CPacket.outgoing.accept": "Accepted the invitation, your opponent will go first", - "c2cpacket.startTicTacToeGameC2CPacket.outgoing.invited": "You invited %s to a game of tic-tac-toe", + "c2cpacket.startTwoPlayerGameC2CPacket.incoming": "%s invited you to a game of %s", + "c2cpacket.startTwoPlayerGameC2CPacket.incoming.accept": "Accept", + "c2cpacket.startTwoPlayerGameC2CPacket.incoming.accept.hover": "Click to accept", + "c2cpacket.startTwoPlayerGameC2CPacket.incoming.accepted": "%s has accepted your invitation to %s", + "c2cpacket.startTwoPlayerGameC2CPacket.outgoing.accept": "Accepted the invitation, your opponent will go first", + "c2cpacket.startTwoPlayerGameC2CPacket.outgoing.invited": "You invited %s to a game of %s", "chorusManip.goalTooFar": "Goal is too far away!", "chorusManip.landing.failed": "Landing manipulation not possible", @@ -62,6 +60,8 @@ "commands.ccalcstack.success.empty.exact": "%s items is exactly %s stacks", "commands.ccalcstack.success.exact": "%s %s is exactly %s stacks", + "commands.cconnectfour.name": "Connect Four", + "commands.ccrackrng.failed": "Failed to crack player seed", "commands.ccrackrng.failed.help": "Help: RNG manipulation doesn't work on some modded servers, in particular Paper.", "commands.ccrackrng.retries": "Cracking player seed, attempt %s/%s", @@ -248,8 +248,7 @@ "commands.ctask.stop.noMatch": "No matching tasks", "commands.ctask.stop.success": "Stopped %s tasks", - "commands.ctictactoe.noGameWithPlayer": "Currently not playing a game with that player", - "commands.ctictactoe.playerNotFound": "Player not found", + "commands.ctictactoe.name": "Tic-tac-toe", "commands.ctime.reset.success": "The time now matches the server", @@ -291,6 +290,16 @@ "commands.cwiki.failed": "Could not retrieve wiki content", "commands.cwiki.noContent": "There is no introductory paragraph in that article", + "connectFourGame.draw": "Draw!", + "connectFourGame.lost": "Lost!", + "connectFourGame.opponentMove": "You are waiting for your opponent...", + "connectFourGame.pieceRed": "red", + "connectFourGame.pieceSet": "You are playing with the %s pieces", + "connectFourGame.pieceYellow": "yellow", + "connectFourGame.title": "Connect Four against %s", + "connectFourGame.won": "Won!", + "connectFourGame.yourMove": "It is your move", + "enchCrack.addInfo": "Add Info", "enchCrack.bookshelfCount": "Bookshelf Count: %s", "enchCrack.clues": "Clues:", @@ -368,5 +377,13 @@ "ticTacToeGame.crosses": "crosses (X)", "ticTacToeGame.noughts": "noughts (O)", "ticTacToeGame.playingWith": "You are playing with %s", - "ticTacToeGame.title": "Tic-tac-toe against %s" + "ticTacToeGame.title": "Tic-tac-toe against %s", + + "twoPlayerGame.chat.draw": "You've made a move and drawn against %s in %s", + "twoPlayerGame.chat.lost": "%s has made a move in %s and won", + "twoPlayerGame.chat.won": "You made a move in %s and beat %s!", + "twoPlayerGame.clickToMakeYourMove": "Click here to make your move", + "twoPlayerGame.incoming": "%s has made a move in %s", + "twoPlayerGame.noGameWithPlayer": "Currently not playing a game with that player", + "twoPlayerGame.playerNotFound": "Player not found" } diff --git a/src/main/resources/assets/clientcommands/lang/zh_cn.json b/src/main/resources/assets/clientcommands/lang/zh_cn.json index 2da6ca248..4e609c3d2 100644 --- a/src/main/resources/assets/clientcommands/lang/zh_cn.json +++ b/src/main/resources/assets/clientcommands/lang/zh_cn.json @@ -4,16 +4,8 @@ "c2cpacket.messageC2CPacket.incoming": "%s -> 你:%s", "c2cpacket.messageC2CPacket.outgoing": "你 -> %s:%s", "c2cpacket.publicKeyNotFound": "未找到公共密匙", - "c2cpacket.putTicTacToeMarkC2CPacket.incoming": "%s已下棋,点击来下你的棋", - "c2cpacket.putTicTacToeMarkC2CPacket.incoming.lost": "%s走了一步棋并赢了", "c2cpacket.receivedC2CPacket": "你收到了一个客户端到客户端数据包,但你不接受传入客户端到客户端的数据包!悬停以查看原始数据包。", "c2cpacket.sentC2CPacket": "你发送了一个客户端到客户端数据包,但你不接受传入客户端到客户端的数据包!", - "c2cpacket.startTicTacToeGameC2CPacket.incoming": "%s邀请你参加井字棋", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accept": "接受", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accept.hover": "点击以接受", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accepted": "%s已接受你的邀请,点击即可开始下棋", - "c2cpacket.startTicTacToeGameC2CPacket.outgoing.accept": "接受了邀请,你的对手将先走", - "c2cpacket.startTicTacToeGameC2CPacket.outgoing.invited": "你邀请%s参加井字棋", "chorusManip.goalTooFar": "目标过远!", "chorusManip.landing.failed": "降落操作不可用", @@ -235,9 +227,6 @@ "commands.ctask.stop.noMatch": "没有匹配的任务", "commands.ctask.stop.success": "已停止%s个任务", - "commands.ctictactoe.noGameWithPlayer": "目前没有与该玩家一起进行游戏", - "commands.ctictactoe.playerNotFound": "未找到玩家", - "commands.ctime.reset.success": "当前时间与服务器相同", "commands.ctitle.cleared": "已清除的标题", diff --git a/src/main/resources/assets/clientcommands/lang/zh_tw.json b/src/main/resources/assets/clientcommands/lang/zh_tw.json index abf82c121..3209dc4fe 100644 --- a/src/main/resources/assets/clientcommands/lang/zh_tw.json +++ b/src/main/resources/assets/clientcommands/lang/zh_tw.json @@ -5,16 +5,8 @@ "c2cpacket.messageC2CPacket.outgoing": "您 -> %s: %s", "c2cpacket.messageTooLong": "訊息過長(最多 255 個字元),已輸入 %s 個字元", "c2cpacket.publicKeyNotFound": "找不到公開金鑰", - "c2cpacket.putTicTacToeMarkC2CPacket.incoming": "%s 在井字遊戲中進行了一步,點選進行您的回合", - "c2cpacket.putTicTacToeMarkC2CPacket.incoming.lost": "%s 在井字遊戲中進行了一步並獲勝", "c2cpacket.receivedC2CPacket": "您收到了一個 C2C 封包,但您沒有接受接收 C2C 封包! 懸停以檢視原始封包。", "c2cpacket.sentC2CPacket": "您發送了一個 C2C 封包,但您沒有接受接收 C2C 封包!", - "c2cpacket.startTicTacToeGameC2CPacket.incoming": "%s 邀請您玩井字遊戲", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accept": "接受", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accept.hover": "點選接受", - "c2cpacket.startTicTacToeGameC2CPacket.incoming.accepted": "%s 已接受您的邀請,點選進行您的回合", - "c2cpacket.startTicTacToeGameC2CPacket.outgoing.accept": "已接受邀請,您的對手將先行動", - "c2cpacket.startTicTacToeGameC2CPacket.outgoing.invited": "您邀請 %s 玩井字遊戲", "chorusManip.goalTooFar": "目標距離過遠!", "chorusManip.landing.failed": "無法操控著陸", @@ -244,9 +236,6 @@ "commands.ctask.stop.noMatch": "沒有符合的任務", "commands.ctask.stop.success": "已停止 %s 個任務", - "commands.ctictactoe.noGameWithPlayer": "目前沒有與該玩家進行遊戲", - "commands.ctictactoe.playerNotFound": "找不到玩家", - "commands.ctime.reset.success": "時間現在與伺服器同步", "commands.ctitle.cleared": "已清除標題", diff --git a/src/main/resources/assets/clientcommands/textures/connect_four/board.png b/src/main/resources/assets/clientcommands/textures/connect_four/board.png new file mode 100644 index 000000000..69a9e4fee Binary files /dev/null and b/src/main/resources/assets/clientcommands/textures/connect_four/board.png differ diff --git a/src/main/resources/assets/clientcommands/textures/connect_four/pieces.png b/src/main/resources/assets/clientcommands/textures/connect_four/pieces.png new file mode 100644 index 000000000..4be52e4ef Binary files /dev/null and b/src/main/resources/assets/clientcommands/textures/connect_four/pieces.png differ diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index 409b38d74..3777cf456 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -63,12 +63,13 @@ }, "client": [ "c2c.ChatListenerMixin", + "c2c.ClientPacketListenerMixin", "commands.alias.ClientSuggestionProviderMixin", "commands.enchant.MultiPlayerGameModeMixin", "commands.findblock.ClientLevelMixin", - "commands.reply.ClientPacketListenerMixin", "commands.generic.CommandSuggestionsMixin", "commands.glow.LivingEntityRenderStateMixin", + "commands.reply.ClientPacketListenerMixin", "commands.snap.MinecraftMixin", "dataqueryhandler.ClientPacketListenerMixin", "events.ClientPacketListenerMixin",