diff --git a/gradle.properties b/gradle.properties index 26d640ad6..0dd79bd5b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ org.gradle.jvmargs=-Xmx2G # Dependencies # also check this on https://fabricmc.net/develop/ fabric_version=0.97.8+1.20.6 - clientarguments_version=1.8.2 + clientarguments_version=1.8.3 betterconfig_version=1.2.1 seedfinding_core_version=1.192.1 seedfinding_biome_version=1.171.1 diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 8f4aa22c8..830978ac8 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -162,6 +162,7 @@ public static void registerCommands(CommandDispatcher SnakeCommand.register(dispatcher); StartupCommand.register(dispatcher); TaskCommand.register(dispatcher); + TicTacToeCommand.register(dispatcher); TooltipCommand.register(dispatcher, context); TranslateCommand.register(dispatcher); UsageTreeCommand.register(dispatcher); diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java index 9ed9df6f7..701240f2c 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketHandler.java @@ -3,11 +3,20 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; 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.packets.PutTicTacToeMarkC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.StartTicTacToeGameC2CPacket; import net.earthcomputer.clientcommands.command.ListenCommand; +import net.earthcomputer.clientcommands.interfaces.IClientPacketListener_C2C; +import net.earthcomputer.clientcommands.command.TicTacToeCommand; import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.minecraft.ChatFormatting; +import net.minecraft.SharedConstants; import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.AccountProfileKeyPairManager; +import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.network.ConnectionProtocol; import net.minecraft.network.FriendlyByteBuf; @@ -19,19 +28,28 @@ import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.ProtocolInfoBuilder; +import net.minecraft.world.entity.player.ProfileKeyPair; import net.minecraft.world.entity.player.ProfilePublicKey; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; import java.security.PublicKey; +import java.util.Arrays; +import java.util.Optional; public class C2CPacketHandler implements C2CPacketListener { + private static final Logger LOGGER = LogUtils.getLogger(); private static final DynamicCommandExceptionType MESSAGE_TOO_LONG_EXCEPTION = new DynamicCommandExceptionType(d -> Component.translatable("c2cpacket.messageTooLong", d)); private static final SimpleCommandExceptionType PUBLIC_KEY_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("c2cpacket.publicKeyNotFound")); private static final SimpleCommandExceptionType ENCRYPTION_FAILED_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("c2cpacket.encryptionFailed")); - public static final ProtocolInfo C2C = ProtocolInfoBuilder.protocolUnbound(ConnectionProtocol.PLAY, PacketFlow.CLIENTBOUND, builder -> builder + public static final ProtocolInfo.Unbound PROTOCOL_UNBOUND = ProtocolInfoBuilder.protocolUnbound(ConnectionProtocol.PLAY, PacketFlow.CLIENTBOUND, builder -> builder .addPacket(MessageC2CPacket.ID, MessageC2CPacket.CODEC) - ).bind(RegistryFriendlyByteBuf.decorator(Minecraft.getInstance().getConnection().registryAccess())); + .addPacket(StartTicTacToeGameC2CPacket.ID, StartTicTacToeGameC2CPacket.CODEC) + .addPacket(PutTicTacToeMarkC2CPacket.ID, PutTicTacToeMarkC2CPacket.CODEC) + ); private static final C2CPacketHandler instance = new C2CPacketHandler(); @@ -48,15 +66,23 @@ public void sendPacket(Packet packet, PlayerInfo recipient) t throw PUBLIC_KEY_NOT_FOUND_EXCEPTION.create(); } ProfilePublicKey ppk = session.profilePublicKey(); + //noinspection ConstantValue if (ppk == null) { throw PUBLIC_KEY_NOT_FOUND_EXCEPTION.create(); } PublicKey key = ppk.data().key(); FriendlyByteBuf buf = PacketByteBufs.create(); - C2C.codec().encode(buf, packet); + ProtocolInfo protocolInfo = getCurrentProtocolInfo(); + if (protocolInfo == null) { + return; + } + protocolInfo.codec().encode(buf, packet); byte[] uncompressed = new byte[buf.readableBytes()]; buf.getBytes(0, uncompressed); byte[] compressed = ConversionHelper.Gzip.compress(uncompressed); + if (compressed == null) { + return; + } // split compressed into 245 byte chunks int chunks = (compressed.length + 244) / 245; byte[][] chunked = new byte[chunks][]; @@ -81,7 +107,7 @@ public void sendPacket(Packet packet, PlayerInfo recipient) t } String packetString = ConversionHelper.BaseUTF8.toUnicode(joined); String commandString = "w " + recipient.getProfile().getName() + " CCENC:" + packetString; - if (commandString.length() >= 256) { + if (commandString.length() >= SharedConstants.MAX_CHAT_LENGTH) { throw MESSAGE_TOO_LONG_EXCEPTION.create(commandString.length()); } ListenCommand.onPacket(packet, ListenCommand.PacketFlow.C2C_OUTBOUND); @@ -89,6 +115,69 @@ public void sendPacket(Packet packet, PlayerInfo recipient) t OutgoingPacketFilter.addPacket(packetString); } + public static boolean handleC2CPacket(String content) { + byte[] encrypted = ConversionHelper.BaseUTF8.fromUnicode(content); + // round down to multiple of 256 bytes + int length = encrypted.length & ~0xFF; + // copy to new array of arrays + byte[][] encryptedArrays = new byte[length / 256][]; + for (int i = 0; i < length; i += 256) { + encryptedArrays[i / 256] = Arrays.copyOfRange(encrypted, i, i + 256); + } + if (!(Minecraft.getInstance().getProfileKeyPairManager() instanceof AccountProfileKeyPairManager profileKeyPairManager)) { + return false; + } + Optional keyPair = profileKeyPairManager.keyPair.join(); + if (keyPair.isEmpty()) { + return false; + } + // decrypt + int len = 0; + byte[][] decryptedArrays = new byte[encryptedArrays.length][]; + for (int i = 0; i < encryptedArrays.length; i++) { + decryptedArrays[i] = ConversionHelper.RsaEcb.decrypt(encryptedArrays[i], keyPair.get().privateKey()); + if (decryptedArrays[i] == null) { + return false; + } + len += decryptedArrays[i].length; + } + // copy to new array + byte[] decrypted = new byte[len]; + int pos = 0; + for (byte[] decryptedArray : decryptedArrays) { + System.arraycopy(decryptedArray, 0, decrypted, pos, decryptedArray.length); + pos += decryptedArray.length; + } + byte[] uncompressed = ConversionHelper.Gzip.decompress(decrypted); + if (uncompressed == null) { + return false; + } + ProtocolInfo protocolInfo = getCurrentProtocolInfo(); + if (protocolInfo == null) { + return false; + } + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(uncompressed)); + Packet packet; + try { + packet = protocolInfo.codec().decode(buf); + } catch (Throwable e) { + LOGGER.error("Error decoding C2C packet", e); + return false; + } + if (buf.readableBytes() > 0) { + LOGGER.error("Found extra bytes while reading C2C packet {}", packet.type()); + return false; + } + ListenCommand.onPacket(packet, ListenCommand.PacketFlow.C2C_INBOUND); + try { + packet.handle(C2CPacketHandler.getInstance()); + } catch (Throwable e) { + Minecraft.getInstance().gui.getChat().addMessage(Component.nullToEmpty(e.getMessage())); + LOGGER.error("Error handling C2C packet", e); + } + return true; + } + @Override public void onMessageC2CPacket(MessageC2CPacket packet) { String sender = packet.sender(); @@ -103,13 +192,32 @@ public void onMessageC2CPacket(MessageC2CPacket packet) { } @Override - public PacketFlow flow() { - return C2C.flow(); + public void onStartTicTacToeGameC2CPacket(StartTicTacToeGameC2CPacket packet) { + TicTacToeCommand.onStartTicTacToeGameC2CPacket(packet); + } + + @Override + public void onPutTicTacToeMarkC2CPacket(PutTicTacToeMarkC2CPacket packet) { + TicTacToeCommand.onPutTicTacToeMarkC2CPacket(packet); + } + + @Nullable + public static ProtocolInfo getCurrentProtocolInfo() { + ClientPacketListener connection = Minecraft.getInstance().getConnection(); + if (connection == null) { + return null; + } + return ((IClientPacketListener_C2C) connection).clientcommands_getC2CProtocolInfo(); + } + + @Override + public @NotNull PacketFlow flow() { + return PacketFlow.CLIENTBOUND; } @Override - public ConnectionProtocol protocol() { - return C2C.id(); + public @NotNull ConnectionProtocol protocol() { + return ConnectionProtocol.PLAY; } @Override diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java index 72948f914..e4a52b1a8 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacketListener.java @@ -1,8 +1,14 @@ package net.earthcomputer.clientcommands.c2c; import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.PutTicTacToeMarkC2CPacket; +import net.earthcomputer.clientcommands.c2c.packets.StartTicTacToeGameC2CPacket; import net.minecraft.network.PacketListener; public interface C2CPacketListener extends PacketListener { void onMessageC2CPacket(MessageC2CPacket packet); + + void onStartTicTacToeGameC2CPacket(StartTicTacToeGameC2CPacket packet); + + void onPutTicTacToeMarkC2CPacket(PutTicTacToeMarkC2CPacket packet); } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/ConversionHelper.java b/src/main/java/net/earthcomputer/clientcommands/c2c/ConversionHelper.java index e6ecc6a69..e02dd042f 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/ConversionHelper.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/ConversionHelper.java @@ -1,5 +1,9 @@ package net.earthcomputer.clientcommands.c2c; +import com.mojang.logging.LogUtils; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + import javax.crypto.Cipher; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -12,6 +16,7 @@ import java.util.zip.GZIPOutputStream; public class ConversionHelper { + private static final Logger LOGGER = LogUtils.getLogger(); /** * @author Wagyourtail @@ -91,7 +96,7 @@ private static int unmapCodepoint(int codepoint) { * @author Wagyourtail */ public static class Gzip { - public static byte[] compress(byte[] bytes) { + public static byte @Nullable [] compress(byte[] bytes) { if (bytes == null || bytes.length == 0) { return null; } @@ -99,12 +104,13 @@ public static byte[] compress(byte[] bytes) { try (GZIPOutputStream gzip = new GZIPOutputStream(out)) { gzip.write(bytes); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error compressing", e); + return null; } return out.toByteArray(); } - public static byte[] uncompress(byte[] bytes) { + public static byte @Nullable [] decompress(byte[] bytes) { if (bytes == null || bytes.length == 0) { return null; } @@ -117,7 +123,8 @@ public static byte[] uncompress(byte[] bytes) { out.write(buffer, 0, n); } } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error decompressing", e); + return null; } return out.toByteArray(); } @@ -130,7 +137,7 @@ public static byte[] encrypt(byte[] bytes, PublicKey key) { cipher.init(Cipher.ENCRYPT_MODE, key); return cipher.doFinal(bytes); } catch (GeneralSecurityException e) { - e.printStackTrace(); + LOGGER.error("Error encrypting", e); return null; } } @@ -141,7 +148,7 @@ public static byte[] decrypt(byte[] bytes, PrivateKey key) { cipher.init(Cipher.DECRYPT_MODE, key); return cipher.doFinal(bytes); } catch (GeneralSecurityException e) { - e.printStackTrace(); + LOGGER.error("Error decrypting", e); return null; } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java new file mode 100644 index 000000000..1bac5f3a8 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/PutTicTacToeMarkC2CPacket.java @@ -0,0 +1,35 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +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; + +public record PutTicTacToeMarkC2CPacket(String sender, byte x, byte y) implements Packet { + public static final StreamCodec CODEC = Packet.codec(PutTicTacToeMarkC2CPacket::write, PutTicTacToeMarkC2CPacket::new); + public static final PacketType ID = new PacketType<>(PacketFlow.CLIENTBOUND, new ResourceLocation("clientcommands", "put_tic_tac_toe_mark")); + + public PutTicTacToeMarkC2CPacket(FriendlyByteBuf buf) { + this(buf.readUtf(), buf.readByte(), buf.readByte()); + } + + public void write(FriendlyByteBuf buf) { + buf.writeUtf(this.sender); + buf.writeByte(this.x); + buf.writeByte(this.y); + } + + @Override + public void handle(C2CPacketListener handler) { + handler.onPutTicTacToeMarkC2CPacket(this); + } + + @Override + public PacketType> type() { + return ID; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTicTacToeGameC2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTicTacToeGameC2CPacket.java new file mode 100644 index 000000000..5d375ae5f --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/StartTicTacToeGameC2CPacket.java @@ -0,0 +1,34 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +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; + +public record StartTicTacToeGameC2CPacket(String sender, boolean accept) implements Packet { + public static final StreamCodec CODEC = Packet.codec(StartTicTacToeGameC2CPacket::write, StartTicTacToeGameC2CPacket::new); + public static final PacketType ID = new PacketType<>(PacketFlow.CLIENTBOUND, new ResourceLocation("clientcommands", "start_tic_tac_toe_game")); + + public StartTicTacToeGameC2CPacket(FriendlyByteBuf buf) { + this(buf.readUtf(), buf.readBoolean()); + } + + public void write(FriendlyByteBuf buf) { + buf.writeUtf(this.sender); + 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/command/CGameModeCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/CGameModeCommand.java index a8be8bd6d..4eddd8331 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/CGameModeCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/CGameModeCommand.java @@ -10,7 +10,6 @@ import net.minecraft.network.chat.Component; import net.minecraft.world.level.GameType; -import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; @@ -25,23 +24,20 @@ public class CGameModeCommand { public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("cgamemode") .then(literal("query") - .then(argument("player", gameProfile()) - .executes(ctx -> getPlayerGameMode(ctx.getSource(), getProfileArgument(ctx, "player"))))) + .then(argument("player", gameProfile(true)) + .executes(ctx -> getPlayerGameMode(ctx.getSource(), getSingleProfileArgument(ctx, "player"))))) .then(literal("list") .then(argument("gameMode", enumArg(GameType.class)) .executes(ctx -> listWithGameMode(ctx.getSource(), getEnum(ctx, "gameMode")))))); } - private static int getPlayerGameMode(FabricClientCommandSource source, Collection profiles) throws CommandSyntaxException { - if (profiles.size() != 1) { + private static int getPlayerGameMode(FabricClientCommandSource source, GameProfile player) throws CommandSyntaxException { + PlayerInfo playerInfo = source.getClient().getConnection().getPlayerInfo(player.getId()); + if (playerInfo == null) { throw PLAYER_NOT_FOUND_EXCEPTION.create(); } - PlayerInfo player = source.getClient().getConnection().getOnlinePlayers().stream() - .filter(p -> p.getProfile().getName().equalsIgnoreCase(profiles.iterator().next().getName())) - .findAny() - .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); - source.sendFeedback(Component.translatable("commands.cgamemode.playerGameMode", player.getProfile().getName(), player.getGameMode().getShortDisplayName())); + source.sendFeedback(Component.translatable("commands.cgamemode.playerGameMode", playerInfo.getProfile().getName(), playerInfo.getGameMode().getShortDisplayName())); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java b/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java index de310dd3a..f252b0a1f 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/ClientCommandHelper.java @@ -13,6 +13,10 @@ import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MutableComponent; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + public class ClientCommandHelper { public static T getFlag(CommandContext ctx, Flag flag) { @@ -55,7 +59,7 @@ public static void addOverlayMessage(Component message, int time) { public static Component getLookCoordsTextComponent(BlockPos pos) { return getCommandTextComponent(Component.translatable("commands.client.blockpos", pos.getX(), pos.getY(), pos.getZ()), - String.format("/clook block %d %d %d", pos.getX(), pos.getY(), pos.getZ())); + String.format("/clook block %d %d %d", pos.getX(), pos.getY(), pos.getZ())); } public static Component getLookCoordsTextComponent(MutableComponent component, BlockPos pos) { @@ -64,7 +68,7 @@ public static Component getLookCoordsTextComponent(MutableComponent component, B public static Component getGlowCoordsTextComponent(BlockPos pos) { return getCommandTextComponent(Component.translatable("commands.client.blockpos", pos.getX(), pos.getY(), pos.getZ()), - String.format("/cglow block %d %d %d 10", pos.getX(), pos.getY(), pos.getZ())); + String.format("/cglow block %d %d %d 10", pos.getX(), pos.getY(), pos.getZ())); } public static Component getGlowCoordsTextComponent(MutableComponent component, BlockPos pos) { @@ -77,8 +81,19 @@ public static Component getCommandTextComponent(@Translatable String translation public static Component getCommandTextComponent(MutableComponent component, String command) { return component.withStyle(style -> style.applyFormat(ChatFormatting.UNDERLINE) - .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(command)))); + .withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(command)))); } + public static final Map runnables = new HashMap<>(); + + public static String registerCode(Runnable code) { + String randomString = new Random().ints('0', 'z' + 1) + .filter(i -> (i <= '9' || i >= 'A') && (i <= 'Z' || i >= 'a')) + .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/command/PingCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/PingCommand.java index 424103b2a..c0b669263 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/PingCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/PingCommand.java @@ -11,8 +11,6 @@ import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.network.chat.Component; -import java.util.Collection; - import static dev.xpple.clientarguments.arguments.CGameProfileArgument.*; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; @@ -20,13 +18,12 @@ public class PingCommand { private static final SimpleCommandExceptionType PLAYER_IN_SINGLEPLAYER_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cping.singleplayer")); private static final SimpleCommandExceptionType PLAYER_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cping.playerNotFound")); - private static final SimpleCommandExceptionType TOO_MANY_PLAYERS_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cping.tooManyPlayers")); public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("cping") .executes(ctx -> printPing(ctx.getSource())) - .then(argument("player", gameProfile()) - .executes(ctx -> printPing(ctx.getSource(), getProfileArgument(ctx, "player")))) + .then(argument("player", gameProfile(true)) + .executes(ctx -> printPing(ctx.getSource(), getSingleProfileArgument(ctx, "player")))) ); } @@ -44,28 +41,18 @@ private static int printPing(FabricClientCommandSource source) throws CommandSyn return Command.SINGLE_SUCCESS; } - private static int printPing(FabricClientCommandSource source, Collection profiles) throws CommandSyntaxException { + private static int printPing(FabricClientCommandSource source, GameProfile player) throws CommandSyntaxException { if (source.getClient().isLocalServer()) { throw PLAYER_IN_SINGLEPLAYER_EXCEPTION.create(); } - if (profiles.isEmpty()) { - throw PLAYER_NOT_FOUND_EXCEPTION.create(); - } - if (profiles.size() > 1) { - throw TOO_MANY_PLAYERS_EXCEPTION.create(); - } - GameProfile profile = profiles.iterator().next(); - - ClientPacketListener packetListener = source.getClient().getConnection(); - assert packetListener != null; - PlayerInfo player = packetListener.getPlayerInfo(profile.getId()); - if (player == null) { + PlayerInfo playerInfo = source.getClient().getConnection().getPlayerInfo(player.getId()); + if (playerInfo == null) { throw PLAYER_NOT_FOUND_EXCEPTION.create(); } - int ping = player.getLatency(); - source.sendFeedback(Component.translatable("commands.cping.success.other", profile.getName(), ping)); + int ping = playerInfo.getLatency(); + source.sendFeedback(Component.translatable("commands.cping.success.other", playerInfo.getProfile().getName(), ping)); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java new file mode 100644 index 000000000..3526c1f7d --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/TicTacToeCommand.java @@ -0,0 +1,280 @@ +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.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.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 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.*; + +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().tell(() -> source.getClient().setScreen(new TicTacToeGameScreen(game))); + return Command.SINGLE_SUCCESS; + } + + public static void onPutTicTacToeMarkC2CPacket(PutTicTacToeMarkC2CPacket packet) { + String sender = packet.sender(); + TicTacToeGame game = activeGames.get(sender); + 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; + } + 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 final PlayerInfo opponent; + + private final Mark[][] board = new Mark[3][3]; + private final Mark yourMarks; + private boolean yourTurn; + + public TicTacToeGame(PlayerInfo opponent, Mark yourMarks) { + this.opponent = opponent; + this.yourMarks = yourMarks; + this.yourTurn = yourMarks == Mark.CROSS; + } + + public boolean putMark(byte x, byte y, Mark mark) { + if (this.yourMarks == mark == this.yourTurn) { + if (this.board[x][y] == null) { + this.board[x][y] = mark; + this.yourTurn = !this.yourTurn; + return true; + } + } + return false; + } + + 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]) { + return this.board[x][0]; + } + if (this.board[0][x] == this.board[1][x] && this.board[1][x] == this.board[2][x]) { + return this.board[0][x]; + } + } + if (this.board[0][0] == this.board[1][1] && this.board[1][1] == this.board[2][2]) { + return this.board[0][0]; + } + if (this.board[0][2] == this.board[1][1] && this.board[1][1] == this.board[2][0]) { + return this.board[0][2]; + } + return null; + } + + private enum Mark { + NOUGHT(Component.translatable("ticTacToeGame.noughts")), + CROSS(Component.translatable("ticTacToeGame.crosses")); + + private final Component name; + + Mark(Component name) { + this.name = name; + } + + public Mark opposite() { + return this == NOUGHT ? CROSS : NOUGHT; + } + } + } + + private static class TicTacToeGameScreen extends Screen { + private final TicTacToeGame game; + + private static final ResourceLocation GRID_TEXTURE = new ResourceLocation("clientcommands", "textures/tic_tac_toe/grid.png"); + private static final ResourceLocation MARKS_TEXTURE = new ResourceLocation("clientcommands", "textures/tic_tac_toe/marks.png"); + + private static final int GRID_SIZE_TEXTURE = 512; + private static final int MARK_SIZE_TEXTURE = 152; + + private static final int GRID_SIZE = 256; + private static final int CELL_SIZE = 80; + private static final int BORDER_SIZE = 8; + private static final int MARK_SIZE = 76; + private static final int PADDING = 2; + + private TicTacToeGameScreen(TicTacToeGame game) { + super(Component.translatable("ticTacToeGame.title", game.opponent.getProfile().getName())); + this.game = game; + } + + @Override + public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + int startX = (this.width - GRID_SIZE) / 2; + int startY = (this.height - GRID_SIZE) / 2; + + 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(GRID_TEXTURE, startX, startY, GRID_SIZE, GRID_SIZE, 0, 0, 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++) { + for (byte y = 0; y < 3; y++) { + TicTacToeGame.Mark mark = board[x][y]; + if (mark == null) { + continue; + } + int offset = switch (mark) { + case NOUGHT -> 0; + case CROSS -> MARK_SIZE_TEXTURE; + }; + guiGraphics.blit(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); + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int startX = (this.width - GRID_SIZE) / 2; + int startY = (this.height - GRID_SIZE) / 2; + if (mouseX < startX || mouseX > startX + GRID_SIZE || mouseY < startY || mouseY > startY + GRID_SIZE) { + return super.mouseClicked(mouseX, mouseY, button); + } + if (button != InputConstants.MOUSE_BUTTON_LEFT) { + return false; + } + double relativeX = mouseX - startX; + byte x = (byte) (relativeX / (CELL_SIZE + BORDER_SIZE)); + if (relativeX > (CELL_SIZE + BORDER_SIZE) * (x + 1) - BORDER_SIZE) { + return false; + } + double relativeY = mouseY - startY; + byte y = (byte) (relativeY / (CELL_SIZE + BORDER_SIZE)); + if (relativeY > (CELL_SIZE + BORDER_SIZE) * (y + 1) - BORDER_SIZE) { + return false; + } + + if (this.game.putMark(x, y, this.game.yourMarks)) { + try { + PutTicTacToeMarkC2CPacket packet = new PutTicTacToeMarkC2CPacket(Minecraft.getInstance().getConnection().getLocalGameProfile().getName(), x, y); + C2CPacketHandler.getInstance().sendPacket(packet, this.game.opponent); + } catch (CommandSyntaxException e) { + Minecraft.getInstance().gui.getChat().addMessage(Component.translationArg(e.getRawMessage())); + } + if (this.game.getWinner() == this.game.yourMarks) { + activeGames.remove(this.game.opponent.getProfile().getName()); + } + return true; + } + return false; + } + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java index fed033b65..0bb5ede87 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/WhisperEncryptedCommand.java @@ -13,8 +13,6 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; -import java.util.Collection; - import static com.mojang.brigadier.arguments.StringArgumentType.*; import static dev.xpple.clientarguments.arguments.CGameProfileArgument.*; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; @@ -25,20 +23,16 @@ public class WhisperEncryptedCommand { public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("cwe") - .then(argument("player", gameProfile()) + .then(argument("player", gameProfile(true)) .then(argument("message", greedyString()) - .executes((ctx) -> whisper(ctx.getSource(), getProfileArgument(ctx, "player"), getString(ctx, "message")))))); + .executes((ctx) -> whisper(ctx.getSource(), getSingleProfileArgument(ctx, "player"), getString(ctx, "message")))))); } - private static int whisper(FabricClientCommandSource source, Collection profiles, String message) throws CommandSyntaxException { - assert source.getClient().getConnection() != null; - if (profiles.size() != 1) { + private static int whisper(FabricClientCommandSource source, GameProfile player, String message) throws CommandSyntaxException { + PlayerInfo recipient = source.getClient().getConnection().getPlayerInfo(player.getId()); + if (recipient == null) { throw PLAYER_NOT_FOUND_EXCEPTION.create(); } - PlayerInfo recipient = source.getClient().getConnection().getOnlinePlayers().stream() - .filter(p -> p.getProfile().getName().equalsIgnoreCase(profiles.iterator().next().getName())) - .findFirst() - .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); MessageC2CPacket packet = new MessageC2CPacket(source.getClient().getConnection().getLocalGameProfile().getName(), message); C2CPacketHandler.getInstance().sendPacket(packet, recipient); diff --git a/src/main/java/net/earthcomputer/clientcommands/command/arguments/PacketTypeArgument.java b/src/main/java/net/earthcomputer/clientcommands/command/arguments/PacketTypeArgument.java index 35ebfa4d5..2929d4716 100644 --- a/src/main/java/net/earthcomputer/clientcommands/command/arguments/PacketTypeArgument.java +++ b/src/main/java/net/earthcomputer/clientcommands/command/arguments/PacketTypeArgument.java @@ -10,6 +10,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import io.netty.channel.ChannelPipeline; import net.earthcomputer.clientcommands.c2c.C2CPacketHandler; +import net.earthcomputer.clientcommands.c2c.C2CPacketListener; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientPacketListener; @@ -86,12 +87,17 @@ private static PacketTypes get() { if (connection == null) { return null; } + ProtocolInfo protocolInfo = C2CPacketHandler.getCurrentProtocolInfo(); + if (protocolInfo == null) { + return null; + } + ChannelPipeline pipeline = connection.getConnection().channel.pipeline(); var decoder = (PacketDecoder) pipeline.get("decoder"); var clientbound = packetTypesCache.computeIfAbsent(decoder, k -> getPacketTypes(decoder.protocolInfo)); var encoder = (PacketEncoder) pipeline.get("encoder"); var serverbound = packetTypesCache.computeIfAbsent(encoder, k -> getPacketTypes(encoder.protocolInfo)); - var c2cbound = packetTypesCache.computeIfAbsent("c2c", k -> getPacketTypes(C2CPacketHandler.C2C)); + var c2cbound = packetTypesCache.computeIfAbsent("c2c", k -> getPacketTypes(protocolInfo)); return new PacketTypes(clientbound, serverbound, c2cbound); } } diff --git a/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java b/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java index 16dc61b1c..1ee2e8f9e 100644 --- a/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java +++ b/src/main/java/net/earthcomputer/clientcommands/features/PacketDumper.java @@ -28,6 +28,7 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.PacketDecoder; import net.minecraft.network.PacketEncoder; +import net.minecraft.network.ProtocolInfo; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamEncoder; import net.minecraft.network.protocol.Packet; @@ -76,8 +77,12 @@ public static void dumpPacket(Packet packet, JsonWriter writer) throws IOExce writer.beginArray(); try { if (packet.type().id().getNamespace().equals("clientcommands")) { + ProtocolInfo protocolInfo = C2CPacketHandler.getCurrentProtocolInfo(); + if (protocolInfo == null) { + throw new IOException("Not currently logged in"); + } //noinspection unchecked - C2CPacketHandler.C2C.codec().encode(new PacketDumpByteBuf(writer), (Packet) packet); + protocolInfo.codec().encode(new PacketDumpByteBuf(writer), (Packet) packet); } else { ChannelPipeline pipeline = Minecraft.getInstance().getConnection().getConnection().channel.pipeline(); @SuppressWarnings("unchecked") diff --git a/src/main/java/net/earthcomputer/clientcommands/interfaces/IClientPacketListener_C2C.java b/src/main/java/net/earthcomputer/clientcommands/interfaces/IClientPacketListener_C2C.java new file mode 100644 index 000000000..0cf0376b4 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/interfaces/IClientPacketListener_C2C.java @@ -0,0 +1,8 @@ +package net.earthcomputer.clientcommands.interfaces; + +import net.earthcomputer.clientcommands.c2c.C2CPacketListener; +import net.minecraft.network.ProtocolInfo; + +public interface IClientPacketListener_C2C { + ProtocolInfo clientcommands_getC2CProtocolInfo(); +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatComponentMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatComponentMixin.java index c89d85059..723c3b4f9 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatComponentMixin.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ChatComponentMixin.java @@ -1,23 +1,15 @@ package net.earthcomputer.clientcommands.mixin.c2c; -import io.netty.buffer.Unpooled; import net.earthcomputer.clientcommands.Configs; import net.earthcomputer.clientcommands.c2c.C2CPacketHandler; -import net.earthcomputer.clientcommands.c2c.C2CPacketListener; -import net.earthcomputer.clientcommands.c2c.ConversionHelper; import net.earthcomputer.clientcommands.c2c.OutgoingPacketFilter; -import net.earthcomputer.clientcommands.command.ListenCommand; import net.minecraft.ChatFormatting; import net.minecraft.client.GuiMessageTag; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.ChatComponent; -import net.minecraft.client.multiplayer.AccountProfileKeyPairManager; -import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.MessageSignature; -import net.minecraft.network.protocol.Packet; -import net.minecraft.world.entity.player.ProfileKeyPair; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -26,9 +18,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.Arrays; -import java.util.Optional; - @Mixin(ChatComponent.class) public class ChatComponentMixin { @@ -60,60 +49,10 @@ private void handleIfPacket(Component content, CallbackInfo ci) { ci.cancel(); return; } - if (handleC2CPacket(packetString)) { + if (C2CPacketHandler.handleC2CPacket(packetString)) { ci.cancel(); } else { this.minecraft.gui.getChat().addMessage(Component.translatable("c2cpacket.malformedPacket").withStyle(ChatFormatting.RED)); } } - - @Unique - private static boolean handleC2CPacket(String content) { - byte[] encrypted = ConversionHelper.BaseUTF8.fromUnicode(content); - // round down to multiple of 256 bytes - int length = encrypted.length & ~0xFF; - // copy to new array of arrays - byte[][] encryptedArrays = new byte[length / 256][]; - for (int i = 0; i < length; i += 256) { - encryptedArrays[i / 256] = Arrays.copyOfRange(encrypted, i, i + 256); - } - if (!(Minecraft.getInstance().getProfileKeyPairManager() instanceof AccountProfileKeyPairManager profileKeyPairManager)) { - return false; - } - Optional keyPair = profileKeyPairManager.keyPair.join(); - if (keyPair.isEmpty()) { - return false; - } - // decrypt - int len = 0; - byte[][] decryptedArrays = new byte[encryptedArrays.length][]; - for (int i = 0; i < encryptedArrays.length; i++) { - decryptedArrays[i] = ConversionHelper.RsaEcb.decrypt(encryptedArrays[i], keyPair.get().privateKey()); - if (decryptedArrays[i] == null) { - return false; - } - len += decryptedArrays[i].length; - } - // copy to new array - byte[] decrypted = new byte[len]; - int pos = 0; - for (byte[] decryptedArray : decryptedArrays) { - System.arraycopy(decryptedArray, 0, decrypted, pos, decryptedArray.length); - pos += decryptedArray.length; - } - byte[] uncompressed = ConversionHelper.Gzip.uncompress(decrypted); - FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(uncompressed)); - Packet packet = C2CPacketHandler.C2C.codec().decode(buf); - if (buf.readableBytes() > 0) { - return false; - } - ListenCommand.onPacket(packet, ListenCommand.PacketFlow.C2C_INBOUND); - try { - packet.handle(C2CPacketHandler.getInstance()); - } catch (Exception e) { - Minecraft.getInstance().gui.getChat().addMessage(Component.nullToEmpty(e.getMessage())); - e.printStackTrace(); - } - return true; - } } 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..abc03ea95 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/c2c/ClientPacketListenerMixin.java @@ -0,0 +1,28 @@ +package net.earthcomputer.clientcommands.mixin.c2c; + +import net.earthcomputer.clientcommands.c2c.C2CPacketHandler; +import net.earthcomputer.clientcommands.c2c.C2CPacketListener; +import net.earthcomputer.clientcommands.interfaces.IClientPacketListener_C2C; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.ProtocolInfo; +import net.minecraft.network.RegistryFriendlyByteBuf; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ClientPacketListener.class) +public class ClientPacketListenerMixin implements IClientPacketListener_C2C { + @Shadow + @Final + private RegistryAccess.Frozen registryAccess; + + @Unique + private final ProtocolInfo c2cProtocolInfo = C2CPacketHandler.PROTOCOL_UNBOUND.bind(RegistryFriendlyByteBuf.decorator(registryAccess)); + + @Override + public ProtocolInfo clientcommands_getC2CProtocolInfo() { + return c2cProtocolInfo; + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/events/ScreenMixin.java b/src/main/java/net/earthcomputer/clientcommands/mixin/events/ScreenMixin.java new file mode 100644 index 000000000..619da805f --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/events/ScreenMixin.java @@ -0,0 +1,34 @@ +package net.earthcomputer.clientcommands.mixin.events; + +import net.earthcomputer.clientcommands.command.ClientCommandHelper; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.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 ScreenMixin { + @Inject(method = "handleComponentClicked", 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 = ClientCommandHelper.runnables.get(value); + if (runnable == null) { + return; + } + runnable.run(); + cir.setReturnValue(true); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/util/MultiVersionCompat.java b/src/main/java/net/earthcomputer/clientcommands/util/MultiVersionCompat.java index d221d3e0b..7ad545a8b 100644 --- a/src/main/java/net/earthcomputer/clientcommands/util/MultiVersionCompat.java +++ b/src/main/java/net/earthcomputer/clientcommands/util/MultiVersionCompat.java @@ -5,9 +5,6 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import net.fabricmc.loader.api.FabricLoader; -import net.fabricmc.loader.api.ModContainer; -import net.fabricmc.loader.api.Version; -import net.fabricmc.loader.api.VersionParsingException; import net.minecraft.SharedConstants; import net.minecraft.Util; import net.minecraft.client.Minecraft; @@ -41,26 +38,13 @@ public boolean doesItemExist(Item item) { return true; } - private static final Version V3_0_6 = Util.make(() -> { - try { - return Version.parse("3.0.6"); - } catch (VersionParsingException e) { - throw new AssertionError(e); - } - }); - public static final MultiVersionCompat INSTANCE = Util.make(() -> { try { FabricLoader loader = FabricLoader.getInstance(); - ModContainer modContainer; if (loader.isModLoaded("viafabric")) { return new ViaFabric(); - } else if ((modContainer = loader.getModContainer("viafabricplus").orElse(null)) != null) { - if (modContainer.getMetadata().getVersion().compareTo(V3_0_6) > 0) { - return new ViaFabricPlus(); // VFP 3.1.0 and newer - } else { - return new ViaFabricPlus3_0_6AndOlder(); // Once 1.20.5 is released, this can be removed - } + } else if (loader.isModLoaded("viafabricplus")) { + return new ViaFabricPlus(); } else { return new None(); } @@ -189,34 +173,6 @@ private int doGetProtocolVersion() throws ReflectiveOperationException { } } - private static final class ViaFabricPlus3_0_6AndOlder extends AbstractViaVersion { - private final Method getTargetVersion; - private final Method versionEnumGetProtocol; - private final Method itemRegistryDiffKeepItem; - - private ViaFabricPlus3_0_6AndOlder() throws ReflectiveOperationException { - Class protocolHack = Class.forName("de.florianmichael.viafabricplus.protocolhack.ProtocolHack"); - Class itemRegistryDiff = Class.forName("de.florianmichael.viafabricplus.fixes.data.ItemRegistryDiff"); - getTargetVersion = protocolHack.getMethod("getTargetVersion"); - versionEnumGetProtocol = getTargetVersion.getReturnType().getMethod("getProtocol"); - itemRegistryDiffKeepItem = itemRegistryDiff.getMethod("keepItem", Item.class); - } - - @Override - protected Object getCurrentVersion() throws ReflectiveOperationException { - return versionEnumGetProtocol.invoke(getTargetVersion.invoke(null)); - } - - @Override - public boolean doesItemExist(Item item) { - try { - return (Boolean) itemRegistryDiffKeepItem.invoke(null, item); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - } - private static final class ViaFabricPlus extends AbstractViaVersion { private final Method getTargetVersion; private final Method olderThan; diff --git a/src/main/resources/assets/clientcommands/lang/de_de.json b/src/main/resources/assets/clientcommands/lang/de_de.json index 62c4e8b00..7156ad1a2 100644 --- a/src/main/resources/assets/clientcommands/lang/de_de.json +++ b/src/main/resources/assets/clientcommands/lang/de_de.json @@ -172,7 +172,6 @@ "commands.cping.success.other": "Der durchschnittliche Ping von %s ist %dms", "commands.cping.singleplayer": "Du bist in einer Einzelspielerwelt (╯°□°)╯︵ ┻━┻", "commands.cping.playerNotFound": "Spieler nicht gefunden", - "commands.cping.tooManyPlayers": "Deine Abfrage kann maximal 1 Spieler betreffen", "commands.cpos.level.overworld": "die Oberwelt", "commands.cpos.level.the_end": "das Ende", diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index e948aad6c..f01a3a258 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -157,7 +157,7 @@ "commands.clisten.list": "Listening to the following packets:", "commands.clisten.clear": "No longer listening to any packets", - "commands.cminesweeper.too_many_mines": "Too many mines, must be between 0 and 9 less than than the amount of total tiles", + "commands.cminesweeper.tooManyMines": "Too many mines, must be between 0 and 9 less than than the amount of total tiles", "commands.cplayerinfo.ioException": "An error occurred", "commands.cplayerinfo.getNameHistory.success": "%s has had the following names: %s", @@ -175,7 +175,6 @@ "commands.cping.success.other": "The average ping of %s is %dms", "commands.cping.singleplayer": "You are in a singleplayer world (╯°□°)╯︵ ┻━┻", "commands.cping.playerNotFound": "Player not found", - "commands.cping.tooManyPlayers": "Your query can match at most 1 player", "commands.cpos.level.overworld": "the Overworld", "commands.cpos.level.the_end": "the End", @@ -205,6 +204,9 @@ "commands.ctask.stop.noMatch": "No matching tasks", "commands.ctask.stop.success": "Stopped %d tasks", + "commands.ctictactoe.playerNotFound": "Player not found", + "commands.ctictactoe.noGameWithPlayer": "Currently not playing a game with that player", + "commands.ctime.reset.success": "The time now matches the server", "commands.ctitle.cleared": "Cleared titles", @@ -337,6 +339,13 @@ "snakeGame.score": "Score: %d", "minesweeperGame.title": "Minesweeper", + "minesweeperGame.timePlayed": "Time Played", + "minesweeperGame.minesLeft": "Mines Left", + + "ticTacToeGame.title": "Tic-tac-toe against %s", + "ticTacToeGame.playingWith": "You are playing with %s", + "ticTacToeGame.noughts": "noughts (O)", + "ticTacToeGame.crosses": "crosses (X)", "c2cpacket.messageTooLong": "Message too long (max. 255 characters) got %s characters", "c2cpacket.publicKeyNotFound": "Public key not found", @@ -346,5 +355,14 @@ "c2cpacket.sentC2CPacket": "You have sent a C2C packet, but you aren't accepting incoming C2C packets!", "c2cpacket.messageC2CPacket.incoming": "%s -> you: %s", - "c2cpacket.messageC2CPacket.outgoing": "you -> %s: %s" + "c2cpacket.messageC2CPacket.outgoing": "you -> %s: %s", + + "c2cpacket.startTicTacToeGameC2CPacket.outgoing.invited": "You invited %s to a game of tic-tac-toe", + "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.putTicTacToeMarkC2CPacket.incoming.lost": "%s has made a move in tic-tac-toe and won", + "c2cpacket.putTicTacToeMarkC2CPacket.incoming": "%s has made a move in tic-tac-toe, click to make your move" } diff --git a/src/main/resources/assets/clientcommands/lang/id_id.json b/src/main/resources/assets/clientcommands/lang/id_id.json index cf8571b4e..238888e30 100644 --- a/src/main/resources/assets/clientcommands/lang/id_id.json +++ b/src/main/resources/assets/clientcommands/lang/id_id.json @@ -157,7 +157,6 @@ "commands.cping.success.other": "Ping rata rata %s adalah %dms", "commands.cping.singleplayer": "Anda berada di dunia singleplayer (╯°□°)╯︵ ┻━┻", "commands.cping.playerNotFound": "Pemain tidak ditemukan", - "commands.cping.tooManyPlayers": "Pencarian anda hanya dapat cocok dengan maksimal 1 pemain", "commands.cpos.level.overworld": "Overworld", "commands.cpos.level.the_end": "End", diff --git a/src/main/resources/assets/clientcommands/lang/ja_jp.json b/src/main/resources/assets/clientcommands/lang/ja_jp.json index 50668cf2f..97a277a1a 100644 --- a/src/main/resources/assets/clientcommands/lang/ja_jp.json +++ b/src/main/resources/assets/clientcommands/lang/ja_jp.json @@ -157,7 +157,6 @@ "commands.cping.success.other": " %s の平均pingは %dms です", "commands.cping.singleplayer": "あなたはシングルプレイヤーワールドにいます (╯°□°)╯︵ ┻━┻", "commands.cping.playerNotFound": "プレイヤーが見つかりませんでした", - "commands.cping.tooManyPlayers": "あなたのクエリは最大でも1人のプレイヤーにしかマッチできません", "commands.cpos.level.overworld": "現世", "commands.cpos.level.the_end": "ジ・エンド", diff --git a/src/main/resources/assets/clientcommands/lang/zh_cn.json b/src/main/resources/assets/clientcommands/lang/zh_cn.json index b021431ca..bcda1d08a 100644 --- a/src/main/resources/assets/clientcommands/lang/zh_cn.json +++ b/src/main/resources/assets/clientcommands/lang/zh_cn.json @@ -170,7 +170,6 @@ "commands.cping.success.other": "%s的平均ping是%dms", "commands.cping.singleplayer": "你在一个单人世界里(╯°□°)╯︵ ┻━┻", "commands.cping.playerNotFound": "未找到玩家", - "commands.cping.tooManyPlayers": "你的查询最多可以匹配1名玩家", "commands.cpos.level.overworld": "主世界", "commands.cpos.level.the_end": "末地", diff --git a/src/main/resources/assets/clientcommands/textures/tic_tac_toe/grid.png b/src/main/resources/assets/clientcommands/textures/tic_tac_toe/grid.png new file mode 100644 index 000000000..e8ee23d53 Binary files /dev/null and b/src/main/resources/assets/clientcommands/textures/tic_tac_toe/grid.png differ diff --git a/src/main/resources/assets/clientcommands/textures/tic_tac_toe/marks.png b/src/main/resources/assets/clientcommands/textures/tic_tac_toe/marks.png new file mode 100644 index 000000000..2979c10b7 Binary files /dev/null and b/src/main/resources/assets/clientcommands/textures/tic_tac_toe/marks.png differ diff --git a/src/main/resources/mixins.clientcommands.json b/src/main/resources/mixins.clientcommands.json index fdc547f97..df7f8eb86 100644 --- a/src/main/resources/mixins.clientcommands.json +++ b/src/main/resources/mixins.clientcommands.json @@ -26,6 +26,7 @@ "debug.EntityMixin", "debug.ServerLevelMixin", "events.MinecraftMixin", + "events.ScreenMixin", "lengthextender.EditBoxMixin", "lengthextender.StringUtilMixin", "rngevents.AnvilMenuMixin", @@ -64,6 +65,7 @@ "requireAnnotations": true }, "client": [ + "c2c.ClientPacketListenerMixin", "commands.alias.ClientSuggestionProviderMixin", "commands.enchant.MultiPlayerGameModeMixin", "commands.findblock.ClientLevelMixin",