diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 13cf3818d..ba6b8bc61 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -83,5 +83,6 @@ public static void registerCommands(CommandDispatcher PosCommand.register(dispatcher); CrackRNGCommand.register(dispatcher); WeatherCommand.register(dispatcher); + ChessCommand.register(dispatcher); } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java index 7f0df495f..f1187cde4 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java @@ -5,15 +5,24 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.logging.LogUtils; import io.netty.buffer.Unpooled; -import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessGame; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import net.earthcomputer.clientcommands.c2c.packets.*; +import net.earthcomputer.clientcommands.command.ChessCommand; +import net.earthcomputer.clientcommands.features.RunnableClickEventActionHelper; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.PlayerListEntry; import net.minecraft.network.PacketByteBuf; 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.joml.Vector2i; import org.slf4j.Logger; import java.security.PublicKey; @@ -28,6 +37,8 @@ public class CCNetworkHandler implements CCPacketListener { private static final Logger LOGGER = LogUtils.getLogger(); + private static final MinecraftClient client = MinecraftClient.getInstance(); + private CCNetworkHandler() { } @@ -81,7 +92,7 @@ public void sendPacket(C2CPacket packet, PlayerListEntry recipient) throws Comma if (commandString.length() >= 256) { throw MESSAGE_TOO_LONG_EXCEPTION.create(commandString.length()); } - MinecraftClient.getInstance().getNetworkHandler().sendChatCommand(commandString); + client.getNetworkHandler().sendChatCommand(commandString); OutgoingPacketFilter.addPacket(packetString); } @@ -95,6 +106,118 @@ public void onMessageC2CPacket(MessageC2CPacket packet) { prefix.append(Text.literal("]").formatted(Formatting.DARK_GRAY)); prefix.append(Text.literal(" ")); Text text = prefix.append(Text.translatable("ccpacket.messageC2CPacket.incoming", sender, message).formatted(Formatting.GRAY)); - MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(text); + client.inGameHud.getChatHud().addMessage(text); + } + + @Override + public void onChessInviteC2CPacket(ChessInviteC2CPacket packet) { + String sender = packet.getSender(); + PlayerListEntry player = client.getNetworkHandler().getPlayerList().stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(sender)) + .findFirst() + .orElse(null); + if (player == null) { + return; + } + ChessTeam chessTeam = packet.getChessTeam(); + if (ChessCommand.currentGame != null) { + try { + ChessAcceptInviteC2CPacket acceptInvitePacket = new ChessAcceptInviteC2CPacket(client.getNetworkHandler().getProfile().getName(), false, chessTeam); + CCNetworkHandler.getInstance().sendPacket(acceptInvitePacket, player); + } catch (CommandSyntaxException e) { + e.printStackTrace(); + return; + } + client.inGameHud.getChatHud().addMessage(Text.translatable("ccpacket.chessInviteC2CPacket.incoming.alreadyInGame", player.getProfile().getName())); + return; + } + MutableText body = Text.translatable("ccpacket.chessInviteC2CPacket.incoming", sender, chessTeam.asString()); + Text accept = Text.translatable("ccpacket.chessInviteC2CPacket.incoming.accept").styled(style -> style + .withFormatting(Formatting.GREEN) + .withClickEvent(new ClickEvent(ClickEvent.Action.CHANGE_PAGE, RunnableClickEventActionHelper.registerCode(() -> { + try { + ChessAcceptInviteC2CPacket acceptInvitePacket = new ChessAcceptInviteC2CPacket(client.getNetworkHandler().getProfile().getName(), true, chessTeam); + CCNetworkHandler.getInstance().sendPacket(acceptInvitePacket, player); + client.inGameHud.getChatHud().addMessage(Text.translatable("ccpacket.chessAcceptInviteC2CPacket.outgoing.accept")); + + ChessCommand.currentGame = new ChessGame(new ChessBoard(), player, chessTeam.other()); + } catch (CommandSyntaxException e) { + e.printStackTrace(); + } + }))) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("ccpacket.chessInviteC2CPacket.incoming.accept.hover")))); + Text deny = Text.translatable("ccpacket.chessInviteC2CPacket.incoming.deny").styled(style -> style + .withFormatting(Formatting.RED) + .withClickEvent(new ClickEvent(ClickEvent.Action.CHANGE_PAGE, RunnableClickEventActionHelper.registerCode(() -> { + try { + ChessAcceptInviteC2CPacket acceptInvitePacket = new ChessAcceptInviteC2CPacket(client.getNetworkHandler().getProfile().getName(), false, chessTeam); + CCNetworkHandler.getInstance().sendPacket(acceptInvitePacket, player); + client.inGameHud.getChatHud().addMessage(Text.translatable("ccpacket.chessAcceptInviteC2CPacket.outgoing.deny")); + } catch (CommandSyntaxException e) { + e.printStackTrace(); + } + }))) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("ccpacket.chessInviteC2CPacket.incoming.deny.hover")))); + Text message = body.append(" ").append(accept).append("/").append(deny); + client.inGameHud.getChatHud().addMessage(message); + } + + @Override + public void onChessAcceptInviteC2CPacket(ChessAcceptInviteC2CPacket packet) { + if (ChessCommand.lastInvitedPlayer == null) { + return; + } + String sender = packet.getSender(); + if (!sender.equals(ChessCommand.lastInvitedPlayer)) { + return; + } + PlayerListEntry opponent = client.getNetworkHandler().getPlayerList().stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(sender)) + .findFirst() + .orElse(null); + if (opponent == null) { + return; + } + boolean accept = packet.isAccept(); + Text text; + if (accept) { + text = Text.translatable("ccpacket.chessAcceptInviteC2CPacket.incoming.accept", sender); + ChessCommand.currentGame = new ChessGame(new ChessBoard(), opponent, packet.getChessTeam()); + } else { + text = Text.translatable("ccpacket.chessAcceptInviteC2CPacket.incoming.deny", sender); + } + client.inGameHud.getChatHud().addMessage(text); + } + + @Override + public void onChessBoardUpdateC2CPacket(ChessBoardUpdateC2CPacket packet) { + if (ChessCommand.currentGame == null) { + return; + } + int fromX = packet.getFromX(); + int fromY = packet.getFromY(); + int toX = packet.getToX(); + int toY = packet.getToY(); + ChessPiece piece = ChessCommand.currentGame.getBoard().getPieceAt(fromX, fromY); + String sender = ChessCommand.currentGame.getOpponent().getProfile().getName(); + Text text; + if (ChessCommand.currentGame.move(piece, new Vector2i(toX, toY))) { + //noinspection ConstantConditions + text = Text.translatable("ccpacket.chessBoardUpdateC2CPacket.incoming", sender, piece.getName(), ChessBoard.indexToFile(toX), toY + 1); + } else { + text = Text.translatable("ccpacket.chessBoardUpdateC2CPacket.incoming.invalid", sender); + } + client.inGameHud.getChatHud().addMessage(text); + } + + @Override + public void onChessResignC2CPacket(ChessResignC2CPacket packet) { + if (ChessCommand.currentGame == null) { + return; + } + String sender = ChessCommand.currentGame.getOpponent().getProfile().getName(); + Text text = Text.translatable("ccpacket.chessResignC2CPacket.incoming", sender); + ChessCommand.currentGame = null; + client.inGameHud.getChatHud().addMessage(text); } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java index 7017c41f0..e774afefd 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.network.PacketByteBuf; import net.minecraft.util.Util; import org.jetbrains.annotations.Nullable; @@ -18,6 +18,10 @@ public class CCPacketHandler { static { CCPacketHandler.register(MessageC2CPacket.class, MessageC2CPacket::new); + CCPacketHandler.register(ChessInviteC2CPacket.class, ChessInviteC2CPacket::new); + CCPacketHandler.register(ChessAcceptInviteC2CPacket.class, ChessAcceptInviteC2CPacket::new); + CCPacketHandler.register(ChessBoardUpdateC2CPacket.class, ChessBoardUpdateC2CPacket::new); + CCPacketHandler.register(ChessResignC2CPacket.class, ChessResignC2CPacket::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..8e8c91770 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketListener.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketListener.java @@ -1,7 +1,15 @@ 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 onChessInviteC2CPacket(ChessInviteC2CPacket packet); + + void onChessAcceptInviteC2CPacket(ChessAcceptInviteC2CPacket packet); + + void onChessBoardUpdateC2CPacket(ChessBoardUpdateC2CPacket packet); + + void onChessResignC2CPacket(ChessResignC2CPacket packet); } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessBoard.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessBoard.java new file mode 100644 index 000000000..1347dd382 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessBoard.java @@ -0,0 +1,104 @@ +package net.earthcomputer.clientcommands.c2c.chess; + +import net.earthcomputer.clientcommands.c2c.chess.pieces.*; +import org.joml.Vector2i; + +import org.jetbrains.annotations.Nullable; + +public class ChessBoard { + + private final ChessPiece[][] pieces = new ChessPiece[8][8]; + + private static final char[] indexToFile = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}; + + public ChessBoard() { + final ChessTeam white = ChessTeam.WHITE; + this.addPiece(0, 0, white, RookChessPiece::new); + this.addPiece(1, 0, white, KnightChessPiece::new); + this.addPiece(2, 0, white, BishopChessPiece::new); + this.addPiece(3, 0, white, QueenChessPiece::new); + this.addPiece(4, 0, white, KingChessPiece::new); + this.addPiece(5, 0, white, BishopChessPiece::new); + this.addPiece(6, 0, white, KnightChessPiece::new); + this.addPiece(7, 0, white, RookChessPiece::new); + for (int x = 0; x < 8; x++) { + this.addPiece(x, 1, white, PawnChessPiece::new); + } + + final ChessTeam black = ChessTeam.BLACK; + this.addPiece(0, 7, black, RookChessPiece::new); + this.addPiece(1, 7, black, KnightChessPiece::new); + this.addPiece(2, 7, black, BishopChessPiece::new); + this.addPiece(3, 7, black, QueenChessPiece::new); + this.addPiece(4, 7, black, KingChessPiece::new); + this.addPiece(5, 7, black, BishopChessPiece::new); + this.addPiece(6, 7, black, KnightChessPiece::new); + this.addPiece(7, 7, black, RookChessPiece::new); + for (int x = 0; x < 8; x++) { + this.addPiece(x, 6, black, PawnChessPiece::new); + } + } + + public

void addPiece(int x, int y, ChessTeam team, ChessPieceFactory

factory) { + this.pieces[x][y] = factory.create(this, team, new Vector2i(x, y)); + } + + public void addPiece(ChessPiece piece) { + Vector2i position = piece.getPosition(); + this.pieces[position.x][position.y] = piece; + } + + ChessPiece[][] getPieces() { + return this.pieces; + } + + public KingChessPiece getKing(ChessTeam team) { + for (ChessPiece[] file : this.pieces) { + for (ChessPiece piece : file) { + if (piece instanceof KingChessPiece king) { + if (king.team == team) { + return king; + } + } + } + } + return null; + } + + @Nullable + public ChessPiece getPieceAt(Vector2i square) { + return this.getPieceAt(square.x, square.y); + } + + @Nullable + public ChessPiece getPieceAt(int x, int y) { + return this.pieces[x][y]; + } + + public boolean isOccupied(Vector2i square) { + return this.getPieceAt(square) != null; + } + + /** + * Try to move a piece. + * @param piece the piece to move + * @param target the square to move to + * @return {@code true} if the move was successful, {@code false} otherwise + */ + public boolean tryMove(ChessPiece piece, Vector2i target) { + if (piece.canMoveTo(target)) { + piece.moveTo(target); + return true; + } + return false; + } + + public static char indexToFile(int x) { + return indexToFile[x]; + } +} + +@FunctionalInterface +interface ChessPieceFactory { + T create(ChessBoard board, ChessTeam team, Vector2i position); +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessGame.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessGame.java new file mode 100644 index 000000000..673f82d9e --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessGame.java @@ -0,0 +1,43 @@ +package net.earthcomputer.clientcommands.c2c.chess; + +import net.minecraft.client.network.PlayerListEntry; +import org.joml.Vector2i; + +public class ChessGame { + + private final ChessBoard board; + private final PlayerListEntry opponent; + private final ChessTeam team; + private ChessTeam teamToPlay = ChessTeam.WHITE; + + public ChessGame(ChessBoard board, PlayerListEntry opponent, ChessTeam team) { + this.board = board; + this.opponent = opponent; + this.team = team; + } + + public ChessBoard getBoard() { + return this.board; + } + + public PlayerListEntry getOpponent() { + return this.opponent; + } + + public ChessTeam getChessTeam() { + return this.team; + } + + public boolean move(ChessPiece piece, Vector2i target) { + if (piece == null) { + return false; + } + if (piece.team == this.teamToPlay) { + if (this.board.tryMove(piece, target)) { + this.teamToPlay = this.teamToPlay.other(); + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessPiece.java new file mode 100644 index 000000000..70c078dc8 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessPiece.java @@ -0,0 +1,108 @@ +package net.earthcomputer.clientcommands.c2c.chess; + +import org.joml.Vector2i; + +public abstract class ChessPiece { + + protected final ChessBoard board; + public final ChessTeam team; + /** + * This position is assigned to upon creation. + * It is always synchronised with the board indices. + */ + private Vector2i position; + + public ChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + this.board = board; + this.team = team; + this.position = position; + } + + /** + * The method to call to determine whether a move can be played. + * @param target the target square + * @return {@code true} if the piece can move to this square, {@code false} otherwise + */ + public final boolean canMoveTo(Vector2i target) { + return this.canMoveTo(target, true); + } + + public final boolean canMoveTo(Vector2i target, boolean checkCheckExposure) { + ChessPiece pieceAtTarget = this.board.getPieceAt(target); + if (pieceAtTarget == null) { + if (!this.isValidMove(target)) { + return false; + } + } else { + if (!this.canCapturePiece(pieceAtTarget)) { + return false; + } + if (!this.isValidAttackMove(target)) { + return false; + } + } + if (this.isPieceInBetween(target)) { + return false; + } + if (checkCheckExposure) { + return !this.moveExposesToCheck(target); + } + return true; + } + + /** + * Checks if a move is valid. + * @param target the target square + * @return {@code true} if the move is generally valid for this piece, {@code false} otherwise + */ + protected boolean isValidMove(Vector2i target) { + return this.getPosition() != target; + } + + /** + * Checks if an attack is valid. + * This method is the same as {@link #isValidMove} for almost all pieces. + * @param target the target square + * @return {@code true} if the attack is generally valid for this piece, {@code false} otherwise + */ + protected boolean isValidAttackMove(Vector2i target) { + return this.isValidMove(target); + } + + protected boolean canCapturePiece(ChessPiece target) { + return this.team != target.team; + } + + protected abstract boolean isPieceInBetween(Vector2i target); + + public boolean moveExposesToCheck(Vector2i target) { + Vector2i oldPosition = this.getPosition(); + this.setPosition(target); + boolean inCheck = this.board.getKing(this.team).isInCheck(); + this.setPosition(oldPosition); + return inCheck; + } + + public void moveTo(Vector2i target) { + this.setPosition(target); + } + + public Vector2i getPosition() { + return this.position; + } + + public void setPosition(Vector2i position) { + ChessPiece[][] boardArray = this.board.getPieces(); + Vector2i currentPosition = this.getPosition(); + this.position = position; + boardArray[currentPosition.x][currentPosition.y] = null; + boardArray[position.x][position.y] = this; + } + + public abstract String getName(); + + /** + * The pieces are based on assets from opengameart.org. + */ + public abstract int getTextureStart(); +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessTeam.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessTeam.java new file mode 100644 index 000000000..e3048e970 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/ChessTeam.java @@ -0,0 +1,26 @@ +package net.earthcomputer.clientcommands.c2c.chess; + +import net.minecraft.util.StringIdentifiable; + +public enum ChessTeam implements StringIdentifiable { + + WHITE("white"), + BLACK("black"); + + public static final com.mojang.serialization.Codec CODEC = StringIdentifiable.createCodec(ChessTeam::values); + + private final String set; + + ChessTeam(String set) { + this.set = set; + } + + public ChessTeam other() { + return this == ChessTeam.WHITE ? ChessTeam.BLACK : ChessTeam.WHITE; + } + + @Override + public String asString() { + return this.set; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/BishopChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/BishopChessPiece.java new file mode 100644 index 000000000..29d48e216 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/BishopChessPiece.java @@ -0,0 +1,46 @@ +package net.earthcomputer.clientcommands.c2c.chess.pieces; + +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import org.joml.Vector2i; + +public class BishopChessPiece extends ChessPiece { + + public BishopChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + super(board, team, position); + } + + @Override + protected boolean isValidMove(Vector2i target) { + if (!super.isValidMove(target)) { + return false; + } + Vector2i difference = target.sub(this.getPosition(), new Vector2i()).absolute(); + return difference.x == difference.y; + } + + @Override + protected boolean isPieceInBetween(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + int incrementX = Integer.signum(difference.x); + int incrementY = Integer.signum(difference.y); + int stop = Math.abs(difference.x); + for (int i = 1, x = incrementX, y = incrementY; i < stop; i++, x += incrementX, y += incrementY) { + if (this.board.getPieceAt(this.getPosition().x + x, this.getPosition().y + y) != null) { + return true; + } + } + return false; + } + + @Override + public String getName() { + return "bishop"; + } + + @Override + public int getTextureStart() { + return 2 * 1024; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/KingChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/KingChessPiece.java new file mode 100644 index 000000000..f2afdcce7 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/KingChessPiece.java @@ -0,0 +1,112 @@ +package net.earthcomputer.clientcommands.c2c.chess.pieces; + +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import org.joml.Vector2i; + +public class KingChessPiece extends ChessPiece { + + private boolean isOnStartingSquare; + + public KingChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + super(board, team, position); + this.isOnStartingSquare = true; + } + + @Override + protected boolean isValidMove(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + Vector2i absDifference = difference.absolute(new Vector2i()); + if (!this.isOnStartingSquare) { + return Math.max(absDifference.x, absDifference.y) == 1; + } + int y = this.team == ChessTeam.WHITE ? 0 : 7; + if (difference.x == -2) { + for (int x = target.x; x <= this.getPosition().x; x++) { + if (this.moveExposesToCheck(new Vector2i(x, y))) { + return false; + } + } + return true; + } + if (difference.x == 2) { + for (int x = this.getPosition().x; x <= target.x; x++) { + if (this.moveExposesToCheck(new Vector2i(x, y))) { + return false; + } + } + return true; + } + return Math.max(difference.x, difference.y) == 1; + } + + @Override + public boolean isValidAttackMove(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()).absolute(); + return Math.max(difference.x, difference.y) == 1; + } + + @Override + protected boolean isPieceInBetween(Vector2i target) { + if (this.isOnStartingSquare) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + if (difference.x == -2) { + return this.board.getPieceAt(this.getPosition().sub(1, 0, new Vector2i())) != null; + } + if (difference.x == 2) { + return this.board.getPieceAt(this.getPosition().add(1, 0, new Vector2i())) != null; + } + } + return false; + } + + @Override + public void moveTo(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + int y = this.team == ChessTeam.WHITE ? 0 : 7; + if (difference.x == -2) { + if (this.board.getPieceAt(0, y) instanceof RookChessPiece rook) { + if (rook.isOnStartingSquare) { + rook.moveTo(new Vector2i(3, y)); + } + } + } else if (difference.x == 2) { + if (this.board.getPieceAt(7, y) instanceof RookChessPiece rook) { + if (rook.isOnStartingSquare) { + rook.moveTo(new Vector2i(5, y)); + } + } + } + super.moveTo(target); + this.isOnStartingSquare = false; + } + + public boolean isInCheck() { + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + ChessPiece piece = board.getPieceAt(x, 7 - y); + if (piece == null) { + continue; + } + if (piece.team == this.team) { + continue; + } + if (piece.canMoveTo(this.getPosition(), false)) { + return true; + } + } + } + return false; + } + + @Override + public String getName() { + return "king"; + } + + @Override + public int getTextureStart() { + return 4 * 1024; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/KnightChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/KnightChessPiece.java new file mode 100644 index 000000000..578568562 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/KnightChessPiece.java @@ -0,0 +1,40 @@ +package net.earthcomputer.clientcommands.c2c.chess.pieces; + +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import org.joml.Vector2i; + +public class KnightChessPiece extends ChessPiece { + + public KnightChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + super(board, team, position); + } + + @Override + protected boolean isValidMove(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()).absolute(); + if (difference.x == 1) { + return difference.y == 2; + } + if (difference.y == 1) { + return difference.x == 2; + } + return false; + } + + @Override + protected boolean isPieceInBetween(Vector2i target) { + return false; + } + + @Override + public String getName() { + return "knight"; + } + + @Override + public int getTextureStart() { + return 1024; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/PawnChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/PawnChessPiece.java new file mode 100644 index 000000000..7fb8fd25c --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/PawnChessPiece.java @@ -0,0 +1,84 @@ +package net.earthcomputer.clientcommands.c2c.chess.pieces; + +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import org.joml.Vector2i; + +public class PawnChessPiece extends ChessPiece { + + private boolean isOnStartingSquare; + + public PawnChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + super(board, team, position); + this.isOnStartingSquare = true; + } + + @Override + public boolean isValidMove(Vector2i target) { + if (this.getPosition().x != target.x) { + return false; + } + int difference; + if (this.team == ChessTeam.WHITE) { + difference = target.y - this.getPosition().y; + } else { + difference = this.getPosition().y - target.y; + } + if (difference == 1) { + return true; + } + if (difference == 2) { + return this.isOnStartingSquare; + } + return false; + } + + @Override + public boolean isValidAttackMove(Vector2i target) { + if (this.getPosition().x - 1 != target.x && this.getPosition().x + 1 != target.x) { + return false; + } + int difference; + if (this.team == ChessTeam.WHITE) { + difference = target.y - this.getPosition().y; + } else { + difference = this.getPosition().y - target.y; + } + return difference == 1; + } + + @Override + protected boolean isPieceInBetween(Vector2i target) { + if (this.isOnStartingSquare) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + if (difference.y == -2) { + return this.board.getPieceAt(this.getPosition().sub(0, 1, new Vector2i())) != null; + } + if (difference.y == 2) { + return this.board.getPieceAt(this.getPosition().add(0, 1, new Vector2i())) != null; + } + } + return false; + } + + @Override + public void moveTo(Vector2i target) { + super.moveTo(target); + this.isOnStartingSquare = false; + int backRank = this.team == ChessTeam.WHITE ? 7 : 0; + if (this.getPosition().y == backRank) { + this.board.addPiece(new QueenChessPiece(this.board, this.team, new Vector2i(this.getPosition()))); + } + } + + @Override + public String getName() { + return "pawn"; + } + + @Override + public int getTextureStart() { + return 5 * 1024; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/QueenChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/QueenChessPiece.java new file mode 100644 index 000000000..933a03583 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/QueenChessPiece.java @@ -0,0 +1,68 @@ +package net.earthcomputer.clientcommands.c2c.chess.pieces; + +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import org.joml.Vector2i; + +public class QueenChessPiece extends ChessPiece { + + public QueenChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + super(board, team, position); + } + + @Override + protected boolean isValidMove(Vector2i target) { + if (!super.isValidMove(target)) { + return false; + } + Vector2i difference = target.sub(this.getPosition(), new Vector2i()).absolute(); + if (difference.x == 0 || difference.y == 0) { + return true; + } + return difference.x == difference.y; + } + + @Override + protected boolean isPieceInBetween(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + if (difference.x == 0) { + int start = Math.min(this.getPosition().y, target.y); + int end = Math.max(this.getPosition().y, target.y); + for (int y = start + 1; y < end; y++) { + if (this.board.getPieceAt(this.getPosition().x, y) != null) { + return true; + } + } + } else if (difference.y == 0) { + int start = Math.min(this.getPosition().x, target.x); + int end = Math.max(this.getPosition().x, target.x); + for (int x = start + 1; x < end; x++) { + if (this.board.getPieceAt(x, this.getPosition().y) != null) { + return true; + } + } + } else { + int incrementX = Integer.signum(difference.x); + int incrementY = Integer.signum(difference.y); + int stop = Math.abs(difference.x); + for (int i = 1, x = incrementX, y = incrementY; i < stop; i++, x += incrementX, y += incrementY) { + if (this.board.getPieceAt(this.getPosition().x + x, this.getPosition().y + y) != null) { + return true; + } + } + return false; + } + return false; + } + + @Override + public String getName() { + return "queen"; + } + + @Override + public int getTextureStart() { + return 3 * 1024; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/RookChessPiece.java b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/RookChessPiece.java new file mode 100644 index 000000000..93df2c2b9 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/chess/pieces/RookChessPiece.java @@ -0,0 +1,61 @@ +package net.earthcomputer.clientcommands.c2c.chess.pieces; + +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import org.joml.Vector2i; + +public class RookChessPiece extends ChessPiece { + + boolean isOnStartingSquare; + + public RookChessPiece(ChessBoard board, ChessTeam team, Vector2i position) { + super(board, team, position); + this.isOnStartingSquare = true; + } + + @Override + protected boolean isValidMove(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + return difference.x == 0 ^ difference.y == 0; + } + + @Override + protected boolean isPieceInBetween(Vector2i target) { + Vector2i difference = target.sub(this.getPosition(), new Vector2i()); + if (difference.x == 0) { + int start = Math.min(this.getPosition().y, target.y); + int end = Math.max(this.getPosition().y, target.y); + for (int y = start + 1; y < end; y++) { + if (this.board.getPieceAt(this.getPosition().x, y) != null) { + return true; + } + } + } else { + int start = Math.min(this.getPosition().x, target.x); + int end = Math.max(this.getPosition().x, target.x); + for (int x = start + 1; x < end; x++) { + if (this.board.getPieceAt(x, this.getPosition().y) != null) { + return true; + } + } + } + return false; + } + + @Override + public void moveTo(Vector2i target) { + super.moveTo(target); + this.isOnStartingSquare = false; + } + + @Override + public String getName() { + return "rook"; + } + + @Override + public int getTextureStart() { + return 0; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessAcceptInviteC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessAcceptInviteC2CPacket.java new file mode 100644 index 000000000..0689800de --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessAcceptInviteC2CPacket.java @@ -0,0 +1,49 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import net.minecraft.network.PacketByteBuf; + +public class ChessAcceptInviteC2CPacket implements C2CPacket { + + private final String sender; + private final boolean accept; + private final ChessTeam chessTeam; + + public ChessAcceptInviteC2CPacket(String sender, boolean accept, ChessTeam chessTeam) { + this.sender = sender; + this.accept = accept; + this.chessTeam = chessTeam; + } + + public ChessAcceptInviteC2CPacket(PacketByteBuf raw) { + this.sender = raw.readString(); + this.accept = raw.readBoolean(); + this.chessTeam = raw.readEnumConstant(ChessTeam.class); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeString(this.sender); + buf.writeBoolean(this.accept); + buf.writeEnumConstant(this.chessTeam); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onChessAcceptInviteC2CPacket(this); + } + + public String getSender() { + return this.sender; + } + + public boolean isAccept() { + return this.accept; + } + + public ChessTeam getChessTeam() { + return this.chessTeam; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessBoardUpdateC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessBoardUpdateC2CPacket.java new file mode 100644 index 000000000..a24ce5dec --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessBoardUpdateC2CPacket.java @@ -0,0 +1,57 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.minecraft.network.PacketByteBuf; +import org.joml.Vector2i; + +public class ChessBoardUpdateC2CPacket implements C2CPacket { + + private final int fromX; + private final int fromY; + private final int toX; + private final int toY; + + public ChessBoardUpdateC2CPacket(Vector2i from, Vector2i to) { + this.fromX = from.x; + this.fromY = from.y; + this.toX = to.x; + this.toY = to.y; + } + + public ChessBoardUpdateC2CPacket(PacketByteBuf raw) { + this.fromX = raw.readInt(); + this.fromY = raw.readInt(); + this.toX = raw.readInt(); + this.toY = raw.readInt(); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeInt(this.fromX); + buf.writeInt(this.fromY); + buf.writeInt(this.toX); + buf.writeInt(this.toY); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onChessBoardUpdateC2CPacket(this); + } + + public int getFromX() { + return this.fromX; + } + + public int getFromY() { + return this.fromY; + } + + public int getToX() { + return this.toX; + } + + public int getToY() { + return this.toY; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessInviteC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessInviteC2CPacket.java new file mode 100644 index 000000000..9480cc3fa --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessInviteC2CPacket.java @@ -0,0 +1,41 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import net.minecraft.network.PacketByteBuf; + +public class ChessInviteC2CPacket implements C2CPacket { + + private final String sender; + private final ChessTeam chessTeam; + + public ChessInviteC2CPacket(String sender, ChessTeam set) { + this.sender = sender; + this.chessTeam = set; + } + + public ChessInviteC2CPacket(PacketByteBuf raw) { + this.sender = raw.readString(); + this.chessTeam = raw.readEnumConstant(ChessTeam.class); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeString(this.sender); + buf.writeEnumConstant(this.chessTeam); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onChessInviteC2CPacket(this); + } + + public String getSender() { + return this.sender; + } + + public ChessTeam getChessTeam() { + return this.chessTeam; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessResignC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessResignC2CPacket.java new file mode 100644 index 000000000..f2d6bf9bf --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/ChessResignC2CPacket.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.minecraft.network.PacketByteBuf; + + +public class ChessResignC2CPacket implements C2CPacket { + + public ChessResignC2CPacket() { + } + + public ChessResignC2CPacket(PacketByteBuf raw) { + } + + @Override + public void write(PacketByteBuf buf) { + } + + @Override + public void apply(CCPacketListener listener) { + listener.onChessResignC2CPacket(this); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ChessCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/ChessCommand.java new file mode 100644 index 000000000..b581f49b0 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/ChessCommand.java @@ -0,0 +1,255 @@ +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 net.earthcomputer.clientcommands.c2c.CCNetworkHandler; +import net.earthcomputer.clientcommands.c2c.chess.ChessBoard; +import net.earthcomputer.clientcommands.c2c.chess.ChessGame; +import net.earthcomputer.clientcommands.c2c.chess.ChessPiece; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import net.earthcomputer.clientcommands.c2c.packets.ChessBoardUpdateC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.ChessInviteC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.ChessResignC2CPacket; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.joml.Vector2i; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL11C; + +import java.util.Collection; + +import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.*; +import static net.earthcomputer.clientcommands.command.arguments.ChessTeamArgumentType.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class ChessCommand { + + private static final SimpleCommandExceptionType PLAYER_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.cchess.playerNotFound")); + private static final SimpleCommandExceptionType ALREADY_IN_GAME_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.cchess.alreadyInGame")); + private static final SimpleCommandExceptionType NOT_IN_GAME_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.cchess.notInGame")); + + public static ChessGame currentGame = null; + public static String lastInvitedPlayer = null; + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("cchess") + .executes(ctx -> openChessGame(ctx.getSource())) + .then(literal("invite") + .then(argument("opponent", gameProfile()) + .executes(ctx -> invitePlayer(ctx.getSource(), getCProfileArgument(ctx, "opponent"))) + .then(argument("team", chessTeam()) + .executes(ctx -> invitePlayer(ctx.getSource(), getCProfileArgument(ctx, "opponent"), getChessTeam(ctx, "team"))))))); + } + + private static int openChessGame(FabricClientCommandSource source) throws CommandSyntaxException { + if (currentGame == null) { + throw NOT_IN_GAME_EXCEPTION.create(); + } + /* + 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 ChessGameScreen(currentGame))); + return Command.SINGLE_SUCCESS; + } + + private static int invitePlayer(FabricClientCommandSource source, Collection profiles) throws CommandSyntaxException { + return invitePlayer(source, profiles, ChessTeam.WHITE); + } + + private static int invitePlayer(FabricClientCommandSource source, Collection profiles, ChessTeam chessSet) throws CommandSyntaxException { + if (currentGame != null) { + throw ALREADY_IN_GAME_EXCEPTION.create(); + } + if (profiles.size() != 1) { + throw PLAYER_NOT_FOUND_EXCEPTION.create(); + } + assert source.getClient().getNetworkHandler() != null; + PlayerListEntry recipient = source.getClient().getNetworkHandler().getPlayerList().stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(profiles.iterator().next().getName())) + .findFirst() + .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); + + ChessInviteC2CPacket packet = new ChessInviteC2CPacket(source.getClient().getNetworkHandler().getProfile().getName(), chessSet); + lastInvitedPlayer = recipient.getProfile().getName(); + CCNetworkHandler.getInstance().sendPacket(packet, recipient); + Text text = Text.translatable("ccpacket.chessInviteC2CPacket.outgoing", recipient.getProfile().getName()); + source.sendFeedback(text); + return Command.SINGLE_SUCCESS; + } +} + +class ChessGameScreen extends Screen { + + private final ChessGame game; + private ChessPiece selectedPiece = null; + + private static final Identifier BOARD_TEXTURE = new Identifier("clientcommands:textures/chess/chess_board.png"); + private static final Identifier PIECES_TEXTURE = new Identifier("clientcommands:textures/chess/pieces.png"); + + private static final int squareWidth = 32; + private static final int squareHeight = 32; + private static final int boardWidth = 8 * squareWidth; + private static final int boardHeight = 8 * squareHeight; + + ChessGameScreen(ChessGame game) { + super(Text.translatable("chessGame.title", game.getOpponent().getProfile().getName())); + this.game = game; + } + + @Override + protected void init() { + int startX = (this.width - boardWidth) / 2; + int startY = (this.height - boardHeight) / 2 + boardHeight; + int padding = 2; + + ButtonWidget resignButton = ButtonWidget.builder(Text.translatable("chessGame.resign"), button -> { + try { + ChessResignC2CPacket packet = new ChessResignC2CPacket(); + CCNetworkHandler.getInstance().sendPacket(packet, ChessCommand.currentGame.getOpponent()); + } catch (CommandSyntaxException e) { + e.printStackTrace(); + } + ChessCommand.currentGame = null; + this.close(); + }) + .dimensions(startX + padding, startY + padding, boardWidth / 2 - 2 * padding, 20) + .build(); + this.addDrawableChild(resignButton); + ButtonWidget shareButton = ButtonWidget.builder(Text.translatable("chessGame.exit"), button -> { + this.close(); + }) + .dimensions(startX + boardWidth / 2 + padding, startY + padding, boardWidth / 2 - 2 * padding, 20) + .build(); + this.addDrawableChild(shareButton); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + super.render(matrices, mouseX, mouseY, delta); + } + + @Override + public void renderBackground(MatrixStack matrices) { + super.renderBackground(matrices); + int startX = (this.width - boardWidth) / 2; + int startY = (this.height - boardHeight) / 2; + + drawTextWithShadow(matrices, client.textRenderer, this.title, startX, startY - 10, 0xff_ffffff); + + RenderSystem.setShader(GameRenderer::getPositionTexProgram); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.setShaderTexture(0, BOARD_TEXTURE); + DrawableHelper.drawTexture(matrices, startX, startY, boardWidth, boardHeight, 0, 0, 2400, 2400, 2400, 2400); + + ChessBoard board = this.game.getBoard(); + RenderSystem.setShaderTexture(0, PIECES_TEXTURE); + int padding = 2; + int textureWidth = squareWidth - (2 * padding); + int textureHeight = squareHeight - (2 * padding); + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + ChessPiece piece = board.getPieceAt(x, 7 - y); + if (piece == null) { + continue; + } + int squareStartX, squareStartY; + if (this.game.getChessTeam() == ChessTeam.WHITE) { + squareStartX = startX + x * squareWidth; + squareStartY = startY + y * squareHeight; + } else { + squareStartX = startX + (7 - x) * squareWidth; + squareStartY = startY + (7 - y) * squareHeight; + } + if (piece == this.selectedPiece) { + DrawableHelper.fill(matrices, squareStartX, squareStartY, squareStartX + squareWidth, squareStartY + squareHeight, 0xff_f21b42); + } + int textureStartX = squareStartX + padding; + int textureStartY = squareStartY + padding; + if (piece.team == ChessTeam.WHITE) { + DrawableHelper.drawTexture(matrices, textureStartX, textureStartY, textureWidth, textureHeight, piece.getTextureStart(), 0, 1024, 1024, 6 * 1024, 1024); + GL11C.glEnable(GL11C.GL_COLOR_LOGIC_OP); + GL11C.glLogicOp(GL11C.GL_INVERT); + DrawableHelper.drawTexture(matrices, textureStartX, textureStartY, textureWidth, textureHeight, piece.getTextureStart(), 0, 1024, 1024, 6 * 1024, 1024); + GL11C.glLogicOp(GL11C.GL_COPY); + GL11C.glDisable(GL11C.GL_COLOR_LOGIC_OP); + } else { + DrawableHelper.drawTexture(matrices, textureStartX, textureStartY, textureWidth, textureHeight, piece.getTextureStart(), 0, 1024, 1024, 6 * 1024, 1024); + } + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (clickedOnBoard(mouseX, mouseY)) { + if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT) { + return false; + } + int startX = (this.width - boardWidth) / 2; + int startY = (this.height - boardHeight) / 2; + int x, y; + if (this.game.getChessTeam() == ChessTeam.WHITE) { + x = (int) Math.round((mouseX - startX)) / squareWidth; + y = 7 - (int) Math.round((mouseY - startY)) / squareHeight; + } else { + x = 7 - (int) Math.round((mouseX - startX)) / squareWidth; + y = (int) Math.round((mouseY - startY)) / squareHeight; + } + if (this.selectedPiece == null) { + ChessPiece piece = this.game.getBoard().getPieceAt(x, y); + if (piece == null) { + this.selectedPiece = null; + } else { + if (piece.team == this.game.getChessTeam()) { + this.selectedPiece = piece; + } + } + } else { + Vector2i oldPosition = this.selectedPiece.getPosition(); + if (this.game.move(this.selectedPiece, new Vector2i(x, y))) { + try { + ChessBoardUpdateC2CPacket packet = new ChessBoardUpdateC2CPacket(oldPosition, new Vector2i(x, y)); + CCNetworkHandler.getInstance().sendPacket(packet, this.game.getOpponent()); + } catch (CommandSyntaxException e) { + e.printStackTrace(); + } + } + this.selectedPiece = null; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + private boolean clickedOnBoard(double mouseX, double mouseY) { + int startX = (this.width - boardWidth) / 2; + int startY = (this.height - boardHeight) / 2; + if (startX > mouseX || mouseX > startX + boardWidth) { + return false; + } + if (startY > mouseY || mouseY > startY + boardHeight) { + return false; + } + return true; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + // TODO: 22/12/2022 implement piece dragging, perhaps arrow drawing + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/ChessTeamArgumentType.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/ChessTeamArgumentType.java new file mode 100644 index 000000000..32ef2fd14 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/ChessTeamArgumentType.java @@ -0,0 +1,21 @@ +package net.earthcomputer.clientcommands.command.arguments; + +import com.mojang.brigadier.context.CommandContext; +import net.earthcomputer.clientcommands.c2c.chess.ChessTeam; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.command.argument.EnumArgumentType; + +public class ChessTeamArgumentType extends EnumArgumentType { + + private ChessTeamArgumentType() { + super(ChessTeam.CODEC, ChessTeam::values); + } + + public static ChessTeamArgumentType chessTeam() { + return new ChessTeamArgumentType(); + } + + public static ChessTeam getChessTeam(CommandContext context, String id) { + return context.getArgument(id, ChessTeam.class); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/features/RunnableClickEventActionHelper.java b/src/main/java/net/earthcomputer/clientcommands/features/RunnableClickEventActionHelper.java new file mode 100644 index 000000000..72c763c7e --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/features/RunnableClickEventActionHelper.java @@ -0,0 +1,20 @@ +package net.earthcomputer.clientcommands.features; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class RunnableClickEventActionHelper { + + public static final Map runnables = new HashMap<>(); + + public static String registerCode(Runnable code) { + String randomString = new Random().ints(48, 122 + 1) // 0 to z + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(10) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + runnables.put(randomString, code); + return randomString; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/MixinScreen.java b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinScreen.java new file mode 100644 index 000000000..f20520f14 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinScreen.java @@ -0,0 +1,35 @@ +package net.earthcomputer.clientcommands.mixin; + +import net.earthcomputer.clientcommands.features.RunnableClickEventActionHelper; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.Style; +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.CallbackInfoReturnable; + +@Mixin(Screen.class) +public class MixinScreen { + + @Inject(method = "handleTextClick", at = @At("HEAD"), cancellable = true) + private void executeCode(Style style, CallbackInfoReturnable cir) { + if (style == null) { + return; + } + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent == null) { + return; + } + if (clickEvent.getAction() != ClickEvent.Action.CHANGE_PAGE) { + return; + } + String value = clickEvent.getValue(); + Runnable runnable = RunnableClickEventActionHelper.runnables.get(value); + if (runnable == null) { + return; + } + runnable.run(); + cir.setReturnValue(true); + } +} diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index 8ccf4dad3..50cd40522 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -38,6 +38,10 @@ "commands.ccalcstack.success.empty.exact": "%d items is exactly %d stacks", "commands.ccalcstack.success.exact": "%d %s is exactly %d stacks", + "commands.cchess.playerNotFound": "Player not found", + "commands.cchess.alreadyInGame": "You are already in a game!", + "commands.cchess.notInGame": "You are currently not in a game!", + "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 %d/%d", @@ -296,6 +300,10 @@ "playerManip.notEnoughItems": "Not enough items(%d of %d) to manipulate seed", "playerManip.uncracked": "Player-Seed needs to be cracked", + "chessGame.title": "Chess game: You vs %s", + "chessGame.resign": "Resign", + "chessGame.exit": "Exit", + "snakeGame.title": "Snake", "snakeGame.score": "Score: %d", @@ -306,6 +314,24 @@ "ccpacket.receivedC2CPacket": "You have received a C2C packet, but you aren't accepting incoming C2C packets! Hover to view the raw packet.", "ccpacket.sentC2CPacket": "You have sent a C2C packet, but you aren't accepting incoming C2C packets!", + "ccpacket.chessAcceptInviteC2CPacket.incoming.accept": "%s has accepted your challenge!", + "ccpacket.chessAcceptInviteC2CPacket.incoming.deny": "%s has denied your challenge!", + "ccpacket.chessAcceptInviteC2CPacket.outgoing.accept": "You have accepted the challenge!", + "ccpacket.chessAcceptInviteC2CPacket.outgoing.deny": "You have denied the challenge!", + + "ccpacket.chessBoardUpdateC2CPacket.incoming": "%s has played: %s to %s%s", + "ccpacket.chessBoardUpdateC2CPacket.incoming.invalid": "%s has played an invalid move!", + + "ccpacket.chessInviteC2CPacket.incoming": "%s (playing as %s) has challenged you to a game of chess!", + "ccpacket.chessInviteC2CPacket.incoming.accept": "Accept", + "ccpacket.chessInviteC2CPacket.incoming.accept.hover": "Click to accept", + "ccpacket.chessInviteC2CPacket.incoming.deny": "Deny", + "ccpacket.chessInviteC2CPacket.incoming.deny.hover": "Click to deny", + "ccpacket.chessInviteC2CPacket.incoming.alreadyInGame": "%s has challenged you to a game of chess, but you are already playing!", + "ccpacket.chessInviteC2CPacket.outgoing": "You have challenged %s to a game of chess!", + + "ccpacket.chessResignC2CPacket.incoming": "%s has resigned!", + "ccpacket.messageC2CPacket.incoming": "%s -> you: %s", "ccpacket.messageC2CPacket.outgoing": "you -> %s: %s" } diff --git a/src/main/resources/assets/clientcommands/textures/chess/chess_board.png b/src/main/resources/assets/clientcommands/textures/chess/chess_board.png new file mode 100644 index 000000000..2f5b4cc2b Binary files /dev/null and b/src/main/resources/assets/clientcommands/textures/chess/chess_board.png differ diff --git a/src/main/resources/assets/clientcommands/textures/chess/pieces.png b/src/main/resources/assets/clientcommands/textures/chess/pieces.png new file mode 100644 index 000000000..84b4b0bfe Binary files /dev/null and b/src/main/resources/assets/clientcommands/textures/chess/pieces.png differ diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index f4ebdaada..5456632fc 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -57,6 +57,7 @@ "MixinProfileKeysImpl", "MixinProtectionEnchantment", "MixinPumpkinBlock", + "MixinScreen", "MixinShearsItem", "MixinStonecutterContainer", "MixinTextFieldWidget",