diff --git a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java index 1736b562e..ef2a26b2a 100644 --- a/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java +++ b/src/main/java/net/earthcomputer/clientcommands/ClientCommands.java @@ -71,6 +71,7 @@ public void onInitializeClient() { public static void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { AuditMixinsCommand.register(dispatcher); BookCommand.register(dispatcher); + DiceRollCommand.register(dispatcher); LookCommand.register(dispatcher); NoteCommand.register(dispatcher); ShrugCommand.register(dispatcher); diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacket.java b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacket.java index b07112279..b64c1ed50 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacket.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/C2CPacket.java @@ -1,9 +1,10 @@ package net.earthcomputer.clientcommands.c2c; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.network.PacketByteBuf; public interface C2CPacket { void write(PacketByteBuf buf); - void apply(CCPacketListener listener); + void apply(CCPacketListener listener) throws CommandSyntaxException; } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java index 8ac6ed1f9..8b17034e6 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCNetworkHandler.java @@ -4,7 +4,10 @@ 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.DiceRollC2CPackets; import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; +import net.earthcomputer.clientcommands.command.DiceRollCommand; import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.PlayerListEntry; @@ -97,4 +100,19 @@ public void onMessageC2CPacket(MessageC2CPacket packet) { Text text = prefix.append(Text.translatable("ccpacket.messageC2CPacket.incoming", sender, message).formatted(Formatting.GRAY)); MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(text); } + + @Override + public void onCoinflipInitC2CPacket(DiceRollC2CPackets.DiceRollInitC2CPacket packet) throws CommandSyntaxException { + DiceRollCommand.initDiceroll(packet); + } + + @Override + public void onCoinflipAcceptedC2CPacket(DiceRollC2CPackets.DiceRollAcceptedC2CPacket packet) throws CommandSyntaxException { + DiceRollCommand.acceptDiceroll(packet); + } + + @Override + public void onCoinflipResultC2CPacket(DiceRollC2CPackets.DiceRollResultC2CPacket packet) { + DiceRollCommand.completeDiceroll(packet); + } } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java index 7017c41f0..2e497d99d 100644 --- a/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/CCPacketHandler.java @@ -2,6 +2,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.earthcomputer.clientcommands.c2c.packets.DiceRollC2CPackets; import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; import net.minecraft.network.PacketByteBuf; import net.minecraft.util.Util; @@ -18,6 +19,7 @@ public class CCPacketHandler { static { CCPacketHandler.register(MessageC2CPacket.class, MessageC2CPacket::new); + DiceRollC2CPackets.register(); } 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..a235f7dc0 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 com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.earthcomputer.clientcommands.c2c.packets.DiceRollC2CPackets; import net.earthcomputer.clientcommands.c2c.packets.MessageC2CPacket; public interface CCPacketListener { void onMessageC2CPacket(MessageC2CPacket packet); + + void onCoinflipInitC2CPacket(DiceRollC2CPackets.DiceRollInitC2CPacket packet) throws CommandSyntaxException; + + void onCoinflipAcceptedC2CPacket(DiceRollC2CPackets.DiceRollAcceptedC2CPacket packet) throws CommandSyntaxException; + + void onCoinflipResultC2CPacket(DiceRollC2CPackets.DiceRollResultC2CPacket packet); } diff --git a/src/main/java/net/earthcomputer/clientcommands/c2c/packets/DiceRollC2CPackets.java b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/DiceRollC2CPackets.java new file mode 100644 index 000000000..8466f6dfc --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/c2c/packets/DiceRollC2CPackets.java @@ -0,0 +1,102 @@ +package net.earthcomputer.clientcommands.c2c.packets; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.earthcomputer.clientcommands.c2c.C2CPacket; +import net.earthcomputer.clientcommands.c2c.CCPacketHandler; +import net.earthcomputer.clientcommands.c2c.CCPacketListener; +import net.minecraft.network.PacketByteBuf; + +import java.math.BigInteger; +import java.util.BitSet; + +public class DiceRollC2CPackets { + // use a diffie hellman key exchange in order to ensure that the coinflip is fair + + public static class DiceRollInitC2CPacket implements C2CPacket { + public final String sender; + public final int sides; + public final byte[] ABHash; + + public DiceRollInitC2CPacket(String sender, int sides, byte[] ABHash) { + this.sender = sender; + this.sides = sides; + this.ABHash = ABHash; + } + + public DiceRollInitC2CPacket(PacketByteBuf raw) { + this.sender = raw.readString(); + this.sides = raw.readInt(); + this.ABHash = raw.readByteArray(); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeString(this.sender); + buf.writeInt(this.sides); + buf.writeByteArray(this.ABHash); + } + + @Override + public void apply(CCPacketListener listener) throws CommandSyntaxException { + listener.onCoinflipInitC2CPacket(this); + } + } + + public static class DiceRollAcceptedC2CPacket implements C2CPacket { + public final String sender; + public final BigInteger AB; + + public DiceRollAcceptedC2CPacket(String sender, BigInteger publicKey) { + this.sender = sender; + this.AB = publicKey; + } + + public DiceRollAcceptedC2CPacket(PacketByteBuf stringBuf) { + this.sender = stringBuf.readString(); + this.AB = new BigInteger(stringBuf.readBitSet().toByteArray()); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeString(this.sender); + buf.writeBitSet(BitSet.valueOf(this.AB.toByteArray())); + } + + @Override + public void apply(CCPacketListener listener) throws CommandSyntaxException { + listener.onCoinflipAcceptedC2CPacket(this); + } + } + + public static class DiceRollResultC2CPacket implements C2CPacket { + public final String sender; + public final BigInteger s; + + public DiceRollResultC2CPacket(String sender, BigInteger s) { + this.sender = sender; + this.s = s; + } + + public DiceRollResultC2CPacket(PacketByteBuf stringBuf) { + this.sender = stringBuf.readString(); + this.s = new BigInteger(stringBuf.readBitSet().toByteArray()); + } + + @Override + public void write(PacketByteBuf buf) { + buf.writeString(this.sender); + buf.writeBitSet(BitSet.valueOf(this.s.toByteArray())); + } + + @Override + public void apply(CCPacketListener listener) { + listener.onCoinflipResultC2CPacket(this); + } + } + + public static void register() { + CCPacketHandler.register(DiceRollInitC2CPacket.class, DiceRollInitC2CPacket::new); + CCPacketHandler.register(DiceRollAcceptedC2CPacket.class, DiceRollAcceptedC2CPacket::new); + CCPacketHandler.register(DiceRollResultC2CPacket.class, DiceRollResultC2CPacket::new); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/command/DiceRollCommand.java b/src/main/java/net/earthcomputer/clientcommands/command/DiceRollCommand.java new file mode 100644 index 000000000..d3b9d56f9 --- /dev/null +++ b/src/main/java/net/earthcomputer/clientcommands/command/DiceRollCommand.java @@ -0,0 +1,279 @@ +package net.earthcomputer.clientcommands.command; + +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.logging.LogUtils; +import net.earthcomputer.clientcommands.c2c.CCNetworkHandler; +import net.earthcomputer.clientcommands.c2c.packets.DiceRollC2CPackets; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.slf4j.Logger; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.*; +import static dev.xpple.clientarguments.arguments.CGameProfileArgumentType.*; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; + +public class DiceRollCommand { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final SimpleCommandExceptionType PLAYER_NOT_FOUND_EXCEPTION = new SimpleCommandExceptionType(Text.translatable("commands.cwe.playerNotFound")); + private static final DynamicCommandExceptionType WAITING_FOR_RESPONSE_EXCEPTION = new DynamicCommandExceptionType(d -> Text.translatable("commands.diceroll.waitingForResponse", d)); + + private static final BigInteger p = new BigInteger(1, + intsToBytes(new int[] { + 0xFFFFFFFF, 0xFFFFFFFF, 0xC90FDAA2, 0x2168C234, 0xC4C6628B, 0x80DC1CD1, + 0x29024E08, 0x8A67CC74, 0x020BBEA6, 0x3B139B22, 0x514A0879, 0x8E3404DD, + 0xEF9519B3, 0xCD3A431B, 0x302B0A6D, 0xF25F1437, 0x4FE1356D, 0x6D51C245, + 0xE485B576, 0x625E7EC6, 0xF44C42E9, 0xA637ED6B, 0x0BFF5CB6, 0xF406B7ED, + 0xEE386BFB, 0x5A899FA5, 0xAE9F2411, 0x7C4B1FE6, 0x49286651, 0xECE65381, + 0xFFFFFFFF, 0xFFFFFFFF + }) + ); + + private static final BigInteger g = BigInteger.TWO; + + + private static final Map opponentHash = new HashMap<>(); + private static final Map rollSides = new HashMap<>(); + private static final Map playerAB = new HashMap<>(); + private static final Map result = new HashMap<>(); + private static final Map timeoutHolder = new HashMap<>(); + + private static final SecureRandom random = new SecureRandom(); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("ccoinflip") + .executes(ctx -> localDiceroll(ctx.getSource(), 2)) + .then(argument("player", gameProfile()) + .executes(ctx -> diceroll(ctx.getSource(), getCProfileArgument(ctx, "player"), 2)))); + + dispatcher.register(literal("cdiceroll") + .executes(ctx -> localDiceroll(ctx.getSource(), 6)) + .then(argument("sides", integer(2, 100)) + .executes(ctx -> localDiceroll(ctx.getSource(), getInteger(ctx, "sides"))) + .then(argument("player", gameProfile()) + .executes(ctx -> diceroll(ctx.getSource(), getCProfileArgument(ctx, "player"), getInteger(ctx, "sides"))))) + .then(argument("player", gameProfile()) + .executes(ctx -> diceroll(ctx.getSource(), getCProfileArgument(ctx, "player"), 6)))); + + } + + private static int localDiceroll(FabricClientCommandSource source, int sides) { + if (sides == 2) { + source.sendFeedback(random.nextBoolean() ? Text.translatable("commands.diceroll.heads") : Text.translatable("commands.diceroll.tails")); + } else { + source.sendFeedback(Text.literal(Integer.toString(random.nextInt(sides) + 1))); + } + return Command.SINGLE_SUCCESS; + } + + public static byte[] toSHA1(byte[] convertme) { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } + catch(NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + return md.digest(convertme); + } + + private static int diceroll(FabricClientCommandSource source, Collection profiles, int sides) throws CommandSyntaxException { + assert source.getClient().getNetworkHandler() != null; + if (profiles.size() != 1) { + throw PLAYER_NOT_FOUND_EXCEPTION.create(); + } + PlayerListEntry recipient = source.getClient().getNetworkHandler().getPlayerList().stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(profiles.iterator().next().getName())) + .findFirst() + .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); + + if (recipient.getProfile().getName().equals(source.getClient().getNetworkHandler().getProfile().getName())) { + return localDiceroll(source, sides); + } + + BigInteger a = new BigInteger(1025, random).abs(); + BigInteger A = g.modPow(a, p); + playerAB.put(recipient.getProfile().getName(), a); + rollSides.put(recipient.getProfile().getName(), sides); + + if (timeoutHolder.containsKey(recipient.getProfile().getName())) { + throw WAITING_FOR_RESPONSE_EXCEPTION.create(recipient.getProfile().getName()); + } + + DiceRollC2CPackets.DiceRollInitC2CPacket packet = new DiceRollC2CPackets.DiceRollInitC2CPacket(source.getClient().getNetworkHandler().getProfile().getName(), sides, toSHA1(A.toByteArray())); + CCNetworkHandler.getInstance().sendPacket(packet, recipient); + if (sides == 2) { + source.sendFeedback(Text.translatable("commands.diceroll.sent", Text.translatable("commands.diceroll.coinflip"), recipient.getProfile().getName())); + } else { + source.sendFeedback(Text.translatable("commands.diceroll.sent", Text.translatable("commands.diceroll.diceroll", sides), recipient.getProfile().getName())); + } + + timeout(recipient.getProfile().getName(), sides); + + return Command.SINGLE_SUCCESS; + } + + public static String byteArrayToHexString(byte[] b) { + StringBuilder result = new StringBuilder(); + for (byte value : b) { + result.append(Integer.toString((value & 0xff) + 0x100, 16).substring(1)); + } + return result.toString(); + } + + private static void timeout(String recipient, int sides) { + AtomicBoolean timeout = new AtomicBoolean(false); + timeoutHolder.put(recipient, timeout); + // timeout + new Timer().schedule(new TimerTask() { + @Override + public void run() { + MinecraftClient.getInstance().execute(() -> { + if (timeout.get()) { + return; + } + if (playerAB.remove(recipient) != null || + rollSides.remove(recipient) != null || + opponentHash.remove(recipient) != null || + result.remove(recipient) != null) { + if (sides == 2) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.translatable("commands.diceroll.timeout", recipient, Text.translatable("commands.diceroll.coinflip"))); + } else { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.translatable("commands.diceroll.timeout", recipient, Text.translatable("commands.diceroll.diceroll", sides))); + } + } + }); + } + }, 5000L); + } + + + public static void initDiceroll(DiceRollC2CPackets.DiceRollInitC2CPacket packet) throws CommandSyntaxException { + + // get sender from name + PlayerListEntry sender = MinecraftClient.getInstance().getNetworkHandler().getPlayerList().stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(packet.sender)) + .findFirst() + .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); + + opponentHash.put(packet.sender, packet.ABHash); + + if (!playerAB.containsKey(packet.sender)) { + BigInteger b = new BigInteger(1025, random).abs(); + BigInteger B = g.modPow(b, p); + playerAB.put(packet.sender, b); + rollSides.put(packet.sender, packet.sides); + + DiceRollC2CPackets.DiceRollInitC2CPacket response = new DiceRollC2CPackets.DiceRollInitC2CPacket(MinecraftClient.getInstance().getNetworkHandler().getProfile().getName(), packet.sides, toSHA1(B.toByteArray())); + CCNetworkHandler.getInstance().sendPacket(response, sender); + MutableText message; + if (packet.sides == 2) { + message = Text.translatable("commands.diceroll.received", Text.translatable("commands.diceroll.coinflip"), packet.sender); + } else { + message = Text.translatable("commands.diceroll.received", Text.translatable("commands.diceroll.diceroll", packet.sides), packet.sender); + } + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(message); + + timeout(packet.sender, packet.sides); + } + + BigInteger b = playerAB.get(packet.sender); + BigInteger B = g.modPow(b, p); + + DiceRollC2CPackets.DiceRollAcceptedC2CPacket response = new DiceRollC2CPackets.DiceRollAcceptedC2CPacket(MinecraftClient.getInstance().getNetworkHandler().getProfile().getName(), B); + CCNetworkHandler.getInstance().sendPacket(response, sender); + } + + public static void acceptDiceroll(DiceRollC2CPackets.DiceRollAcceptedC2CPacket packet) throws CommandSyntaxException { + if (!opponentHash.containsKey(packet.sender)) { + throw PLAYER_NOT_FOUND_EXCEPTION.create(); + } + + // get sender from name + PlayerListEntry sender = MinecraftClient.getInstance().getNetworkHandler().getPlayerList().stream() + .filter(p -> p.getProfile().getName().equalsIgnoreCase(packet.sender)) + .findFirst() + .orElseThrow(PLAYER_NOT_FOUND_EXCEPTION::create); + + BigInteger a = playerAB.get(packet.sender); + BigInteger B = packet.AB; + + // check if hash matches + if (!Arrays.equals(opponentHash.get(packet.sender), toSHA1(B.toByteArray()))) { + System.out.println("expected: " + byteArrayToHexString(opponentHash.get(packet.sender))); + System.out.println("actual: " + byteArrayToHexString(toSHA1(B.toByteArray()))); + MutableText message = Text.translatable("commands.diceroll.cheater", packet.sender).formatted(Formatting.RED); + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(message); + opponentHash.remove(packet.sender); + playerAB.remove(packet.sender); + rollSides.remove(packet.sender); + return; + } + BigInteger s = B.modPow(a, p); + result.put(packet.sender, s); + + DiceRollC2CPackets.DiceRollResultC2CPacket response = new DiceRollC2CPackets.DiceRollResultC2CPacket(MinecraftClient.getInstance().getNetworkHandler().getProfile().getName(), s); + CCNetworkHandler.getInstance().sendPacket(response, sender); + opponentHash.remove(packet.sender); + playerAB.remove(packet.sender); + } + + public static void completeDiceroll(DiceRollC2CPackets.DiceRollResultC2CPacket packet) { + if (result.containsKey(packet.sender)) { + if (result.get(packet.sender).equals(packet.s)) { + LOGGER.info("Coinflip val: " + packet.s.toString(16)); + MutableText message; + if (rollSides.get(packet.sender) == 2) { + MutableText headstails; + BigInteger half = p.divide(BigInteger.valueOf(2)); + if (packet.s.compareTo(half) > 0) { + headstails = Text.translatable("commands.diceroll.heads"); + } else { + headstails = Text.translatable("commands.diceroll.tails"); + } + message = Text.translatable("commands.diceroll.value", Text.translatable("commands.diceroll.coinflip"), packet.sender, headstails); + } else { + BigInteger sideVal = p.divide(BigInteger.valueOf(rollSides.get(packet.sender))); + int side = packet.s.divide(sideVal).intValue() + 1; + message = Text.translatable("commands.diceroll.value", Text.translatable("commands.diceroll.diceroll", rollSides.get(packet.sender)), packet.sender, side); + } + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(message); + } else { + LOGGER.info("expected: " + result.get(packet.sender).toString(16)); + LOGGER.info("actual: " + packet.s.toString(16)); + MutableText message = Text.translatable("commands.diceroll.cheater", packet.sender).formatted(Formatting.RED); + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(message); + } + timeoutHolder.remove(packet.sender).set(true); + result.remove(packet.sender); + rollSides.remove(packet.sender); + } else { + throw new IllegalStateException("Coinflip result packet received before accepted/init packet"); + } + } + + private static byte[] intsToBytes(int[] ints) { + ByteBuffer buffer = ByteBuffer.allocate(ints.length * Integer.BYTES); + for (int i : ints) { + buffer.putInt(i); + } + return buffer.array(); + } +} diff --git a/src/main/java/net/earthcomputer/clientcommands/mixin/MixinChatHud.java b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinChatHud.java index 086812c5e..3ff3e7060 100644 --- a/src/main/java/net/earthcomputer/clientcommands/mixin/MixinChatHud.java +++ b/src/main/java/net/earthcomputer/clientcommands/mixin/MixinChatHud.java @@ -1,5 +1,7 @@ package net.earthcomputer.clientcommands.mixin; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import io.netty.buffer.Unpooled; import net.earthcomputer.clientcommands.Configs; import net.earthcomputer.clientcommands.c2c.*; @@ -105,6 +107,14 @@ private static boolean handleC2CPacket(String content) { } try { c2CPacket.apply(CCNetworkHandler.getInstance()); + } catch (CommandSyntaxException e) { + Message m = e.getRawMessage(); + if (m instanceof Text t) { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(t); + } else { + MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.of(m.getString())); + } + e.printStackTrace(); } catch (Exception e) { MinecraftClient.getInstance().inGameHud.getChatHud().addMessage(Text.of(e.getMessage())); e.printStackTrace(); diff --git a/src/main/resources/assets/clientcommands/lang/en_us.json b/src/main/resources/assets/clientcommands/lang/en_us.json index 156bee025..f2b607526 100644 --- a/src/main/resources/assets/clientcommands/lang/en_us.json +++ b/src/main/resources/assets/clientcommands/lang/en_us.json @@ -110,6 +110,17 @@ "commands.chotbar.notCreative": "Player must be in creative mode to restore item hotbars", "commands.chotbar.restoredHotbar": "Restored item hotbar %d", + "commands.diceroll.diceroll": "dice roll (%s side)", + "commands.diceroll.coinflip": "coin flip", + "commands.diceroll.sent": "Sent %s request to %s", + "commands.diceroll.received": "Received %s request from %s", + "commands.diceroll.value": "%s result from %s: %s", + "commands.diceroll.heads": "Heads", + "commands.diceroll.tails": "Tails", + "commands.diceroll.cheater": "I think %s is cheating, they got a different value.", + "commands.diceroll.timeout": "Timeout waiting for %s's response to %s request", + "commands.diceroll.waitingForResponse": "Waiting for %s's response already", + "commands.citemgroup.notFound": "Item group \"%s\" not found", "commands.citemgroup.outOfBounds": "Index %d is out of bounds", "commands.citemgroup.saveFile.failed": "Could not save item groups file",