tictactoe and cosmetics system fix
This commit is contained in:
@@ -66,11 +66,15 @@ public class ClientCosmetics {
|
||||
CosmeticProfile profile = PROFILES.get(uuid);
|
||||
if (profile == null) return null;
|
||||
|
||||
if (profile.nameType == NameType.STATIC && profile.staticColor != null) {
|
||||
if (profile.nameType == NameType.STATIC) {
|
||||
if (profile.staticColor == null) return null;
|
||||
return Text.literal(name)
|
||||
.styled(s -> s.withColor(profile.staticColor).withBold(true));
|
||||
}
|
||||
|
||||
// GRADIENT
|
||||
if (profile.gradientStart == null || profile.gradientEnd == null) return null;
|
||||
|
||||
long time = Util.getMeasuringTimeMs();
|
||||
MutableText animated = Text.empty();
|
||||
|
||||
@@ -106,6 +110,7 @@ public class ClientCosmetics {
|
||||
|
||||
public static void fetchMojangCapes(UUID uuid) {
|
||||
try {
|
||||
System.out.println("SZAR: fetching Mojang capes for " + uuid);
|
||||
MinecraftClient client = MinecraftClient.getInstance();
|
||||
String accessToken = client.getSession().getAccessToken();
|
||||
if (accessToken == null) return;
|
||||
@@ -150,6 +155,7 @@ public class ClientCosmetics {
|
||||
buf.writeString(cape.name);
|
||||
buf.writeString(cape.url);
|
||||
}
|
||||
System.out.println("SZAR: found " + list.size() + " Mojang capes, sending to server");
|
||||
|
||||
ClientPlayNetworking.send(MOJANG_CAPES_SYNC, buf);
|
||||
}
|
||||
|
||||
@@ -40,10 +40,7 @@ import net.minecraft.sound.SoundEvent;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.Util;
|
||||
import net.minecraft.util.math.Box;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.util.math.*;
|
||||
import net.minecraft.util.math.random.Random;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
@@ -93,6 +90,28 @@ public class SzarClient implements ClientModInitializer {
|
||||
);
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
// Open screen
|
||||
ClientPlayNetworking.registerGlobalReceiver(Szar.TTT_OPEN_SCREEN, (client, handler, buf, sender) -> {
|
||||
BlockPos pos = buf.readBlockPos();
|
||||
TicTacToeBlockEntity.State state = TicTacToeBlockEntity.readStateFromBuf(buf);
|
||||
client.execute(() -> client.setScreen(new TicTacToeScreen(pos, state)));
|
||||
});
|
||||
ClientPlayNetworking.registerGlobalReceiver(Szar.TTT_CLOSE_SCREEN, (client, handler, buf, sender) -> {
|
||||
client.execute(() -> {
|
||||
if (client.currentScreen instanceof TicTacToeScreen) {
|
||||
client.setScreen(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
ClientPlayNetworking.registerGlobalReceiver(Szar.TTT_STATE_SYNC, (client, handler, buf, sender) -> {
|
||||
BlockPos pos = buf.readBlockPos();
|
||||
TicTacToeBlockEntity.State state = TicTacToeBlockEntity.readStateFromBuf(buf);
|
||||
client.execute(() -> {
|
||||
if (client.currentScreen instanceof TicTacToeScreen screen) {
|
||||
screen.updateState(state);
|
||||
}
|
||||
});
|
||||
});
|
||||
ClientPlayNetworking.registerGlobalReceiver(Szar.DRUNK_TYPE_PACKET, (client, handler, buf, responseSender) -> {
|
||||
String typeName = buf.readString();
|
||||
client.execute(() -> DrunkEffect.setDisplayType(typeName));
|
||||
@@ -242,24 +261,38 @@ public class SzarClient implements ClientModInitializer {
|
||||
ClientPlayNetworking.send(PlayerMovementManager.PACKET_ID, buf);
|
||||
});
|
||||
ClientPlayNetworking.registerGlobalReceiver(SYNC_PACKET, (client, handler, buf, responseSender) -> {
|
||||
// First read the player UUID
|
||||
UUID playerUuid = buf.readUuid();
|
||||
|
||||
// Read cosmetic data
|
||||
NameType nameType = buf.readEnumConstant(NameType.class);
|
||||
Integer staticColor = buf.readBoolean() ? buf.readInt() : null;
|
||||
Integer gradientStart = buf.readBoolean() ? buf.readInt() : null;
|
||||
Integer gradientEnd = gradientStart != null ? buf.readInt() : null;
|
||||
|
||||
String textureUrl = buf.readString();
|
||||
Identifier capeTexture = loadTextureFromURL(textureUrl, playerUuid.toString());
|
||||
|
||||
// Apply the cosmetic profile on the main thread
|
||||
// Load texture off main thread, apply on main thread
|
||||
Identifier capeTexture = textureUrl.isEmpty() ? null
|
||||
: loadTextureFromURL(textureUrl, playerUuid.toString());
|
||||
|
||||
client.execute(() -> {
|
||||
ClientCosmetics.fetchMojangCapes(playerUuid);
|
||||
ClientCosmetics.apply(playerUuid, nameType, staticColor, gradientStart, gradientEnd, capeTexture);
|
||||
// Check BEFORE applying if this is first sync for local player
|
||||
boolean isFirstSync = client.player != null
|
||||
&& playerUuid.equals(client.player.getUuid())
|
||||
&& ClientCosmetics.get(playerUuid) == null;
|
||||
|
||||
ClientCosmetics.apply(playerUuid, nameType, staticColor,
|
||||
gradientStart, gradientEnd, capeTexture);
|
||||
|
||||
if (isFirstSync) {
|
||||
java.util.concurrent.CompletableFuture.runAsync(() ->
|
||||
ClientCosmetics.fetchMojangCapes(playerUuid));
|
||||
}
|
||||
});
|
||||
});
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
||||
if (client.player == null) return;
|
||||
UUID uuid = client.player.getUuid();
|
||||
java.util.concurrent.CompletableFuture.runAsync(() ->
|
||||
ClientCosmetics.fetchMojangCapes(uuid));
|
||||
});
|
||||
ClientPlayNetworking.registerGlobalReceiver(Szar.OPEN_MERL_SCREEN,
|
||||
(client, handler, buf, responseSender) -> {
|
||||
int entityId = buf.readInt();
|
||||
|
||||
139
src/client/java/dev/tggamesyt/szar/client/TicTacToeScreen.java
Normal file
139
src/client/java/dev/tggamesyt/szar/client/TicTacToeScreen.java
Normal file
@@ -0,0 +1,139 @@
|
||||
package dev.tggamesyt.szar.client;
|
||||
|
||||
import dev.tggamesyt.szar.Szar;
|
||||
import dev.tggamesyt.szar.TicTacToeBlockEntity;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class TicTacToeScreen extends Screen {
|
||||
|
||||
private static final Identifier BG = new Identifier("szar", "textures/gui/tictactoe.png");
|
||||
private static final Identifier X_TEX = new Identifier("szar", "textures/gui/x.png");
|
||||
private static final Identifier O_TEX = new Identifier("szar", "textures/gui/o.png");
|
||||
|
||||
private static final int GUI_WIDTH = 176;
|
||||
private static final int GUI_HEIGHT = 166;
|
||||
private static final int BOARD_X = 16; // offset inside GUI
|
||||
private static final int BOARD_Y = 16;
|
||||
private static final int CELL_SIZE = 44;
|
||||
|
||||
private TicTacToeBlockEntity.State state;
|
||||
private final BlockPos blockPos;
|
||||
private final UUID localPlayer;
|
||||
|
||||
// Add field
|
||||
private boolean isSpectator;
|
||||
|
||||
// Update constructor
|
||||
public TicTacToeScreen(BlockPos pos, TicTacToeBlockEntity.State state) {
|
||||
super(Text.literal("Tic Tac Toe"));
|
||||
this.blockPos = pos;
|
||||
this.state = state;
|
||||
this.localPlayer = MinecraftClient.getInstance().player.getUuid();
|
||||
this.isSpectator = state.isSpectator;
|
||||
}
|
||||
|
||||
// Update updateState
|
||||
public void updateState(TicTacToeBlockEntity.State newState) {
|
||||
this.state = newState;
|
||||
this.isSpectator = newState.isSpectator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
renderBackground(context);
|
||||
|
||||
int x = (this.width - GUI_WIDTH) / 2;
|
||||
int y = (this.height - GUI_HEIGHT) / 2;
|
||||
|
||||
// Draw background
|
||||
context.drawTexture(BG, x, y, 0, 0, GUI_WIDTH, GUI_HEIGHT, GUI_WIDTH, GUI_HEIGHT);
|
||||
|
||||
// Draw board cells
|
||||
for (int i = 0; i < 9; i++) {
|
||||
int col = i % 3;
|
||||
int row = i / 3;
|
||||
int cx = x + BOARD_X + col * CELL_SIZE;
|
||||
int cy = y + BOARD_Y + row * CELL_SIZE;
|
||||
|
||||
if (state.board[i] == 1) {
|
||||
// O
|
||||
context.drawTexture(O_TEX, cx + 4, cy + 4, 0, 0,
|
||||
CELL_SIZE - 8, CELL_SIZE - 8,
|
||||
CELL_SIZE - 8, CELL_SIZE - 8);
|
||||
} else if (state.board[i] == 2) {
|
||||
// X
|
||||
context.drawTexture(X_TEX, cx + 4, cy + 4, 0, 0,
|
||||
CELL_SIZE - 8, CELL_SIZE - 8,
|
||||
CELL_SIZE - 8, CELL_SIZE - 8);
|
||||
}
|
||||
}
|
||||
|
||||
// Status text
|
||||
String status;
|
||||
if (state.winner == 1) status = "§bO wins!";
|
||||
else if (state.winner == 2) status = "§cX wins!";
|
||||
else if (state.winner == 3) status = "§eDraw!";
|
||||
else if (isSpectator) status = "§7Spectating...";
|
||||
else {
|
||||
boolean myTurn = (state.currentTurn == 1 && localPlayer.equals(state.player1))
|
||||
|| (state.currentTurn == 2 && localPlayer.equals(state.player2));
|
||||
status = myTurn ? "§aYour turn!" : "§7Opponent's turn...";
|
||||
}
|
||||
context.drawTextWithShadow(this.textRenderer, Text.literal(status),
|
||||
x + GUI_WIDTH / 2 - this.textRenderer.getWidth(status) / 2,
|
||||
y + BOARD_Y + 3 * CELL_SIZE + 8, 0xFFFFFF);
|
||||
|
||||
super.render(context, mouseX, mouseY, delta);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (isSpectator) return super.mouseClicked(mouseX, mouseY, button);
|
||||
if (button != 0) return super.mouseClicked(mouseX, mouseY, button);
|
||||
if (state.winner != 0) return super.mouseClicked(mouseX, mouseY, button);
|
||||
|
||||
// Check if it's our turn
|
||||
boolean myTurn = (state.currentTurn == 1 && localPlayer.equals(state.player1))
|
||||
|| (state.currentTurn == 2 && localPlayer.equals(state.player2));
|
||||
if (!myTurn) return super.mouseClicked(mouseX, mouseY, button);
|
||||
|
||||
int x = (this.width - GUI_WIDTH) / 2;
|
||||
int y = (this.height - GUI_HEIGHT) / 2;
|
||||
|
||||
for (int i = 0; i < 9; i++) {
|
||||
int col = i % 3;
|
||||
int row = i / 3;
|
||||
int cx = x + BOARD_X + col * CELL_SIZE;
|
||||
int cy = y + BOARD_Y + row * CELL_SIZE;
|
||||
|
||||
if (mouseX >= cx && mouseX < cx + CELL_SIZE
|
||||
&& mouseY >= cy && mouseY < cy + CELL_SIZE) {
|
||||
if (state.board[i] == 0) {
|
||||
sendMove(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
private void sendMove(int cell) {
|
||||
PacketByteBuf buf = PacketByteBufs.create();
|
||||
buf.writeBlockPos(blockPos);
|
||||
buf.writeInt(cell);
|
||||
ClientPlayNetworking.send(Szar.TTT_MAKE_MOVE, buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldPause() { return false; }
|
||||
}
|
||||
@@ -57,6 +57,8 @@ public class ServerCosmetics {
|
||||
int size = buf.readInt();
|
||||
List<MojangCape> list = new ArrayList<>();
|
||||
|
||||
System.out.println("SZAR: server received Mojang capes for " + uuid + ", count=" + size);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
MojangCape c = new MojangCape();
|
||||
c.id = buf.readString();
|
||||
@@ -67,25 +69,6 @@ public class ServerCosmetics {
|
||||
|
||||
PLAYER_MOJANG_CAPES.put(uuid, list);
|
||||
});
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||
ServerPlayerEntity player = handler.getPlayer();
|
||||
|
||||
// Send this player's own cosmetics to themselves
|
||||
UserCosmetics user = USERS.get(player.getUuid());
|
||||
if (user != null) {
|
||||
sync(player, user);
|
||||
}
|
||||
|
||||
// Optionally: send all other players' cosmetics to this new player
|
||||
for (ServerPlayerEntity other : server.getPlayerManager().getPlayerList()) {
|
||||
if (other.equals(player)) continue;
|
||||
|
||||
UserCosmetics otherUser = USERS.get(other.getUuid());
|
||||
if (otherUser != null) {
|
||||
sync(player, otherUser); // send other players’ cosmetics to the new player
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- LOAD JSON ---------------- */
|
||||
@@ -152,20 +135,27 @@ public class ServerCosmetics {
|
||||
private static void registerCommand() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
|
||||
dispatcher.register(CommandManager.literal("cape")
|
||||
.then(CommandManager.argument("id", StringArgumentType.greedyString()) // <-- change here
|
||||
.then(CommandManager.argument("id", StringArgumentType.greedyString())
|
||||
.suggests((ctx, builder) -> {
|
||||
|
||||
ServerPlayerEntity player = ctx.getSource().getPlayer();
|
||||
if (player == null) return builder.buildFuture();
|
||||
|
||||
// Custom capes
|
||||
for (String id : CAPES.keySet())
|
||||
builder.suggest(id);
|
||||
UserCosmetics user = USERS.get(player.getUuid());
|
||||
|
||||
// Mojang capes
|
||||
// Only suggest capes this player actually owns
|
||||
if (user != null) {
|
||||
for (String id : user.ownedCapes) {
|
||||
builder.suggest(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Mojang capes from server-side map
|
||||
List<MojangCape> mojang = PLAYER_MOJANG_CAPES.get(player.getUuid());
|
||||
System.out.println("SZAR: suggestions - mojang capes for " + player.getName().getString() + ": " + (mojang == null ? "null" : mojang.size()));
|
||||
if (mojang != null) {
|
||||
for (MojangCape c : mojang)
|
||||
for (MojangCape c : mojang) {
|
||||
builder.suggest(c.name);
|
||||
}
|
||||
}
|
||||
|
||||
builder.suggest("none");
|
||||
@@ -173,10 +163,15 @@ public class ServerCosmetics {
|
||||
})
|
||||
.executes(ctx -> {
|
||||
ServerPlayerEntity player = ctx.getSource().getPlayer();
|
||||
String id = StringArgumentType.getString(ctx, "id"); // this now includes spaces
|
||||
String id = StringArgumentType.getString(ctx, "id");
|
||||
|
||||
UserCosmetics user = USERS.get(player.getUuid());
|
||||
if (user == null) return 0;
|
||||
// Create a default entry if player has no cosmetics profile
|
||||
UserCosmetics user = USERS.computeIfAbsent(player.getUuid(), k -> {
|
||||
UserCosmetics u = new UserCosmetics();
|
||||
u.nameType = NameType.STATIC;
|
||||
u.ownedCapes = new ArrayList<>();
|
||||
return u;
|
||||
});
|
||||
|
||||
// Deselect
|
||||
if (id.equalsIgnoreCase("none")) {
|
||||
@@ -186,12 +181,12 @@ public class ServerCosmetics {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Mojang cape selection (only from fetched list)
|
||||
// Mojang cape selection
|
||||
List<MojangCape> mojang = PLAYER_MOJANG_CAPES.get(player.getUuid());
|
||||
if (mojang != null) {
|
||||
for (MojangCape c : mojang) {
|
||||
if (c.name.equalsIgnoreCase(id)) {
|
||||
user.selectedCape = c.id; // vanilla cape
|
||||
user.selectedCape = c.id;
|
||||
sync(player, user);
|
||||
player.sendMessage(Text.literal("Equipped Mojang cape: " + c.name), false);
|
||||
return 1;
|
||||
@@ -199,7 +194,7 @@ public class ServerCosmetics {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom
|
||||
// Custom cape check
|
||||
if (!user.ownedCapes.contains(id)) {
|
||||
player.sendMessage(Text.literal("You don't own this cape."), false);
|
||||
return 0;
|
||||
@@ -216,49 +211,47 @@ public class ServerCosmetics {
|
||||
|
||||
/* ---------------- SYNC ---------------- */
|
||||
|
||||
public static void sync(ServerPlayerEntity player, UserCosmetics user) {
|
||||
List<ServerPlayerEntity> original =
|
||||
player.getServer().getPlayerManager().getPlayerList();
|
||||
List<ServerPlayerEntity> list = new ArrayList<>(original);
|
||||
if (!list.contains(player)) {list.add(player);}
|
||||
for (ServerPlayerEntity p : list) {
|
||||
PacketByteBuf buf = PacketByteBufs.create();
|
||||
|
||||
// Write player UUID first
|
||||
buf.writeUuid(player.getUuid());
|
||||
|
||||
// Cosmetic data
|
||||
buf.writeEnumConstant(user.nameType);
|
||||
buf.writeBoolean(user.staticColor != null);
|
||||
if (user.staticColor != null) buf.writeInt(user.staticColor);
|
||||
|
||||
buf.writeBoolean(user.gradientStart != null);
|
||||
if (user.gradientStart != null) {
|
||||
buf.writeInt(user.gradientStart);
|
||||
buf.writeInt(user.gradientEnd);
|
||||
}
|
||||
|
||||
String textureUrl = null;
|
||||
if (user.selectedCape != null) {
|
||||
textureUrl = CAPES.get(user.selectedCape);
|
||||
if (textureUrl == null) {
|
||||
List<MojangCape> mojang = PLAYER_MOJANG_CAPES.get(player.getUuid());
|
||||
if (mojang != null) {
|
||||
for (MojangCape c : mojang) {
|
||||
if (c.id.equalsIgnoreCase(user.selectedCape)) {
|
||||
textureUrl = c.url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.writeString(textureUrl == null ? "" : textureUrl);
|
||||
ServerPlayNetworking.send(p, SYNC_PACKET, buf);
|
||||
// Send ONE player's cosmetics to ALL online players (used for /cape changes)
|
||||
public static void sync(ServerPlayerEntity owner, UserCosmetics user) {
|
||||
for (ServerPlayerEntity p : owner.getServer().getPlayerManager().getPlayerList()) {
|
||||
syncTo(p, owner, user);
|
||||
}
|
||||
}
|
||||
public enum NameType {
|
||||
STATIC,
|
||||
GRADIENT
|
||||
}
|
||||
|
||||
// Send ONE player's cosmetics to ONE recipient
|
||||
public static void syncTo(ServerPlayerEntity recipient, ServerPlayerEntity owner,
|
||||
UserCosmetics user) {
|
||||
PacketByteBuf buf = PacketByteBufs.create();
|
||||
buf.writeUuid(owner.getUuid()); // whose cosmetics these are
|
||||
buf.writeEnumConstant(user.nameType);
|
||||
buf.writeBoolean(user.staticColor != null);
|
||||
if (user.staticColor != null) buf.writeInt(user.staticColor);
|
||||
buf.writeBoolean(user.gradientStart != null);
|
||||
if (user.gradientStart != null) {
|
||||
buf.writeInt(user.gradientStart);
|
||||
buf.writeInt(user.gradientEnd);
|
||||
}
|
||||
|
||||
String textureUrl = null;
|
||||
if (user.selectedCape != null) {
|
||||
textureUrl = CAPES.get(user.selectedCape);
|
||||
if (textureUrl == null) {
|
||||
List<MojangCape> mojang = PLAYER_MOJANG_CAPES.get(owner.getUuid());
|
||||
if (mojang != null) {
|
||||
for (MojangCape c : mojang) {
|
||||
if (c.id.equalsIgnoreCase(user.selectedCape)) {
|
||||
textureUrl = c.url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.writeString(textureUrl == null ? "" : textureUrl);
|
||||
ServerPlayNetworking.send(recipient, SYNC_PACKET, buf);
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,7 @@ public class Szar implements ModInitializer {
|
||||
RegistryKeys.DIMENSION_TYPE,
|
||||
new Identifier(MOD_ID, "backrooms")
|
||||
);
|
||||
public static final Map<UUID, BlockPos> tttActivePlayers = new java.util.HashMap<>();
|
||||
public static final Block SZAR_BLOCK =
|
||||
new SzarBlock();
|
||||
public static final Block URANIUM_BLOCK =
|
||||
@@ -383,6 +384,7 @@ public class Szar implements ModInitializer {
|
||||
entries.add(Szar.CAN_OF_BEANS);
|
||||
entries.add(Szar.ALMOND_WATER);
|
||||
entries.add(Szar.KEBAB);
|
||||
entries.add(Szar.TIC_TAC_TOE_ITEM);
|
||||
// crazy weponary
|
||||
entries.add(Szar.BULLET_ITEM);
|
||||
entries.add(Szar.AK47);
|
||||
@@ -611,19 +613,32 @@ public class Szar implements ModInitializer {
|
||||
PlayerMovementManager.init();
|
||||
ServerCosmetics.init();
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
|
||||
ServerPlayerEntity player = handler.getPlayer();
|
||||
ServerPlayerEntity joiner = handler.getPlayer();
|
||||
|
||||
ServerCosmetics.UserCosmetics user = USERS.get(player.getUuid());
|
||||
if (user != null) {
|
||||
|
||||
// AUTO SELECT FIRST CAPE IF NONE SELECTED
|
||||
if (user.selectedCape == null && !user.ownedCapes.isEmpty()) {
|
||||
user.selectedCape = user.ownedCapes.get(0);
|
||||
} else {
|
||||
user.selectedCape = null;
|
||||
// Send joiner's own cosmetics to themselves
|
||||
ServerCosmetics.UserCosmetics joinerUser = USERS.get(joiner.getUuid());
|
||||
if (joinerUser != null) {
|
||||
if (joinerUser.selectedCape == null && !joinerUser.ownedCapes.isEmpty()) {
|
||||
joinerUser.selectedCape = joinerUser.ownedCapes.get(0);
|
||||
}
|
||||
ServerCosmetics.syncTo(joiner, joiner, joinerUser);
|
||||
}
|
||||
|
||||
sync(player, user);
|
||||
// Send all other online players' cosmetics to the joiner
|
||||
for (ServerPlayerEntity other : server.getPlayerManager().getPlayerList()) {
|
||||
if (other.equals(joiner)) continue;
|
||||
ServerCosmetics.UserCosmetics otherUser = USERS.get(other.getUuid());
|
||||
if (otherUser != null) {
|
||||
ServerCosmetics.syncTo(joiner, other, otherUser);
|
||||
}
|
||||
}
|
||||
|
||||
// Send joiner's cosmetics to all other online players
|
||||
if (joinerUser != null) {
|
||||
for (ServerPlayerEntity other : server.getPlayerManager().getPlayerList()) {
|
||||
if (other.equals(joiner)) continue;
|
||||
ServerCosmetics.syncTo(other, joiner, joinerUser);
|
||||
}
|
||||
}
|
||||
});
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
|
||||
@@ -1313,7 +1328,38 @@ public class Szar implements ModInitializer {
|
||||
});
|
||||
ServerTickEvents.END_SERVER_TICK.register(DrunkEffect::tick);
|
||||
FartManager.register();
|
||||
ServerPlayNetworking.registerGlobalReceiver(TTT_MAKE_MOVE, (server, player, handler, buf, sender) -> {
|
||||
BlockPos pos = buf.readBlockPos();
|
||||
int cell = buf.readInt();
|
||||
server.execute(() -> {
|
||||
if (player.getWorld().getBlockEntity(pos) instanceof TicTacToeBlockEntity be) {
|
||||
be.handleMove(player, cell);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static final Block TIC_TAC_TOE_BLOCK = Registry.register(
|
||||
Registries.BLOCK, new Identifier(MOD_ID, "tictactoe"),
|
||||
new TicTacToeBlock(AbstractBlock.Settings.create().strength(2f))
|
||||
);
|
||||
public static final BlockItem TIC_TAC_TOE_ITEM = Registry.register(
|
||||
Registries.ITEM, new Identifier(MOD_ID, "tictactoe"),
|
||||
new BlockItem(TIC_TAC_TOE_BLOCK, new Item.Settings())
|
||||
);
|
||||
public static final BlockEntityType<TicTacToeBlockEntity> TIC_TAC_TOE_ENTITY =
|
||||
Registry.register(
|
||||
Registries.BLOCK_ENTITY_TYPE, new Identifier(MOD_ID, "tictactoe"),
|
||||
FabricBlockEntityTypeBuilder.create(TicTacToeBlockEntity::new,
|
||||
TIC_TAC_TOE_BLOCK).build()
|
||||
);
|
||||
|
||||
// Networking
|
||||
public static final Identifier TTT_OPEN_SCREEN = new Identifier(MOD_ID, "ttt_open");
|
||||
public static final Identifier TTT_MAKE_MOVE = new Identifier(MOD_ID, "ttt_move");
|
||||
public static final Identifier TTT_STATE_SYNC = new Identifier(MOD_ID, "ttt_sync");
|
||||
public static final Identifier TTT_CLOSE_SCREEN = new Identifier(MOD_ID, "ttt_close");
|
||||
|
||||
// Blocks
|
||||
public static final TrackerBlock TRACKER_BLOCK = Registry.register(
|
||||
Registries.BLOCK, new Identifier(MOD_ID, "tracker"),
|
||||
|
||||
82
src/main/java/dev/tggamesyt/szar/TicTacToeBlock.java
Normal file
82
src/main/java/dev/tggamesyt/szar/TicTacToeBlock.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package dev.tggamesyt.szar;
|
||||
|
||||
import net.minecraft.block.*;
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.block.entity.BlockEntityType;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.util.ActionResult;
|
||||
import net.minecraft.util.Hand;
|
||||
import net.minecraft.util.hit.BlockHitResult;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.shape.VoxelShape;
|
||||
import net.minecraft.util.shape.VoxelShapes;
|
||||
import net.minecraft.world.BlockView;
|
||||
import net.minecraft.world.World;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class TicTacToeBlock extends BlockWithEntity {
|
||||
|
||||
public TicTacToeBlock(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockRenderType getRenderType(BlockState state) {
|
||||
return BlockRenderType.MODEL;
|
||||
}
|
||||
|
||||
private static final VoxelShape SHAPE = VoxelShapes.union(
|
||||
VoxelShapes.cuboid(0f, 0f, 0f, 1f, 0.75f, 1f)
|
||||
);
|
||||
|
||||
@Override
|
||||
public VoxelShape getOutlineShape(BlockState state, BlockView world,
|
||||
BlockPos pos, ShapeContext ctx) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoxelShape getCollisionShape(BlockState state, BlockView world,
|
||||
BlockPos pos, ShapeContext ctx) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new TicTacToeBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult onUse(BlockState state, World world, BlockPos pos,
|
||||
PlayerEntity player, Hand hand, BlockHitResult hit) {
|
||||
if (world.isClient) return ActionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayerEntity serverPlayer)) return ActionResult.PASS;
|
||||
if (!(world.getBlockEntity(pos) instanceof TicTacToeBlockEntity be)) return ActionResult.PASS;
|
||||
|
||||
be.handlePlayerJoin(serverPlayer, pos);
|
||||
return ActionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(
|
||||
World world, BlockState state, BlockEntityType<T> type) {
|
||||
if (world.isClient) return null;
|
||||
return type == Szar.TIC_TAC_TOE_ENTITY
|
||||
? (w, pos, s, be) -> TicTacToeBlockEntity.tick(w, pos, s,
|
||||
(TicTacToeBlockEntity) be)
|
||||
: null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBreak(World world, BlockPos pos, BlockState state, PlayerEntity player) {
|
||||
if (!world.isClient && world.getBlockEntity(pos) instanceof TicTacToeBlockEntity be) {
|
||||
be.closeScreenForAll(world.getServer());
|
||||
if (be.player1 != null) Szar.tttActivePlayers.remove(be.player1);
|
||||
if (be.player2 != null) Szar.tttActivePlayers.remove(be.player2);
|
||||
}
|
||||
super.onBreak(world, pos, state, player);
|
||||
}
|
||||
}
|
||||
270
src/main/java/dev/tggamesyt/szar/TicTacToeBlockEntity.java
Normal file
270
src/main/java/dev/tggamesyt/szar/TicTacToeBlockEntity.java
Normal file
@@ -0,0 +1,270 @@
|
||||
package dev.tggamesyt.szar;
|
||||
|
||||
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.block.entity.BlockEntity;
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.network.PacketByteBuf;
|
||||
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class TicTacToeBlockEntity extends BlockEntity {
|
||||
|
||||
// 0 = empty, 1 = O (player1), 2 = X (player2)
|
||||
public int[] board = new int[9];
|
||||
public UUID player1 = null; // O
|
||||
public UUID player2 = null; // X
|
||||
public int currentTurn = 1; // 1 = O's turn, 2 = X's turn
|
||||
public int winner = 0; // 0 = ongoing, 1 = O wins, 2 = X wins, 3 = draw
|
||||
public final java.util.Set<UUID> spectators = new java.util.HashSet<>();
|
||||
public int resetTimer = -1; // -1 = no reset pending
|
||||
public TicTacToeBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Szar.TIC_TAC_TOE_ENTITY, pos, state);
|
||||
}
|
||||
|
||||
public static void tick(World world, BlockPos pos, BlockState state,
|
||||
TicTacToeBlockEntity entity) {
|
||||
if (!world.isClient && entity.resetTimer > 0) {
|
||||
entity.resetTimer--;
|
||||
if (entity.resetTimer == 0) {
|
||||
entity.resetTimer = -1;
|
||||
entity.resetGame(((net.minecraft.server.world.ServerWorld) world).getServer());
|
||||
entity.syncToPlayers(((net.minecraft.server.world.ServerWorld) world).getServer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handlePlayerJoin(ServerPlayerEntity player, BlockPos pos) {
|
||||
UUID uuid = player.getUuid();
|
||||
|
||||
// Check if already in a different game
|
||||
BlockPos activePos = Szar.tttActivePlayers.get(uuid);
|
||||
if (activePos != null && !activePos.equals(pos)) {
|
||||
player.sendMessage(Text.literal("§cYou are already in a game at another board!"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rejoin existing game
|
||||
if (uuid.equals(player1) || uuid.equals(player2)) {
|
||||
openScreen(player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (player1 == null) {
|
||||
player1 = uuid;
|
||||
Szar.tttActivePlayers.put(uuid, pos);
|
||||
player.sendMessage(Text.literal("§aYou are §bO§a! Waiting for second player..."), true);
|
||||
markDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
if (player2 == null && !uuid.equals(player1)) {
|
||||
player2 = uuid;
|
||||
Szar.tttActivePlayers.put(uuid, pos);
|
||||
player.sendMessage(Text.literal("§aYou are §cX§a! Game starting!"), true);
|
||||
|
||||
ServerPlayerEntity p1 = getServer(player).getPlayerManager().getPlayer(player1);
|
||||
if (p1 != null) {
|
||||
p1.sendMessage(Text.literal("§aSecond player joined! Your turn!"), true);
|
||||
}
|
||||
|
||||
openScreenForBoth(player);
|
||||
markDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
// At the bottom where it says "game is full", replace with:
|
||||
if (player1 != null && player2 != null) {
|
||||
spectators.add(uuid);
|
||||
player.sendMessage(Text.literal("§7Spectating the match..."), true);
|
||||
openScreen(player);
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private net.minecraft.server.MinecraftServer getServer(ServerPlayerEntity player) {
|
||||
return player.getServer();
|
||||
}
|
||||
|
||||
public void openScreenForBoth(ServerPlayerEntity joiner) {
|
||||
openScreen(joiner);
|
||||
ServerPlayerEntity p1 = joiner.getServer().getPlayerManager().getPlayer(player1);
|
||||
if (p1 != null) openScreen(p1);
|
||||
}
|
||||
|
||||
public void openScreen(ServerPlayerEntity player) {
|
||||
PacketByteBuf buf = PacketByteBufs.create();
|
||||
buf.writeBlockPos(this.pos);
|
||||
writeStateToBuf(buf, player.getUuid());
|
||||
ServerPlayNetworking.send(player, Szar.TTT_OPEN_SCREEN, buf);
|
||||
}
|
||||
|
||||
public void handleMove(ServerPlayerEntity player, int cell) {
|
||||
if (winner != 0) return;
|
||||
if (cell < 0 || cell > 8) return;
|
||||
if (board[cell] != 0) return;
|
||||
|
||||
UUID uuid = player.getUuid();
|
||||
int playerNum = uuid.equals(player1) ? 1 : uuid.equals(player2) ? 2 : 0;
|
||||
if (playerNum == 0) return;
|
||||
if (playerNum != currentTurn) {
|
||||
player.sendMessage(Text.literal("§cNot your turn!"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
board[cell] = playerNum;
|
||||
currentTurn = (currentTurn == 1) ? 2 : 1;
|
||||
checkWinner();
|
||||
markDirty();
|
||||
syncToPlayers(player.getServer());
|
||||
}
|
||||
|
||||
private void checkWinner() {
|
||||
int[][] lines = {
|
||||
{0,1,2},{3,4,5},{6,7,8},
|
||||
{0,3,6},{1,4,7},{2,5,8},
|
||||
{0,4,8},{2,4,6}
|
||||
};
|
||||
|
||||
for (int[] line : lines) {
|
||||
int a = board[line[0]], b = board[line[1]], c = board[line[2]];
|
||||
if (a != 0 && a == b && b == c) {
|
||||
winner = a;
|
||||
scheduleReset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
boolean full = true;
|
||||
for (int cell : board) {
|
||||
if (cell == 0) { full = false; break; }
|
||||
}
|
||||
if (full) {
|
||||
winner = 3;
|
||||
scheduleReset();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleReset() {
|
||||
resetTimer = 60;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
public void syncToPlayers(net.minecraft.server.MinecraftServer server) {
|
||||
sendToPlayer(server, player1);
|
||||
sendToPlayer(server, player2);
|
||||
for (UUID uuid : spectators) {
|
||||
sendToPlayer(server, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendToPlayer(net.minecraft.server.MinecraftServer server, UUID uuid) {
|
||||
if (uuid == null) return;
|
||||
ServerPlayerEntity p = server.getPlayerManager().getPlayer(uuid);
|
||||
if (p == null) return;
|
||||
PacketByteBuf buf = PacketByteBufs.create();
|
||||
buf.writeBlockPos(this.pos);
|
||||
writeStateToBuf(buf, uuid);
|
||||
ServerPlayNetworking.send(p, Szar.TTT_STATE_SYNC, buf);
|
||||
}
|
||||
|
||||
public void writeStateToBuf(PacketByteBuf buf, UUID viewerUuid) {
|
||||
for (int cell : board) buf.writeInt(cell);
|
||||
buf.writeBoolean(player1 != null);
|
||||
if (player1 != null) buf.writeUuid(player1);
|
||||
buf.writeBoolean(player2 != null);
|
||||
if (player2 != null) buf.writeUuid(player2);
|
||||
buf.writeInt(currentTurn);
|
||||
buf.writeInt(winner);
|
||||
// Is the viewer a spectator?
|
||||
boolean isSpectator = viewerUuid != null
|
||||
&& !viewerUuid.equals(player1)
|
||||
&& !viewerUuid.equals(player2);
|
||||
buf.writeBoolean(isSpectator);
|
||||
}
|
||||
|
||||
public static State readStateFromBuf(PacketByteBuf buf) {
|
||||
State s = new State();
|
||||
s.board = new int[9];
|
||||
for (int i = 0; i < 9; i++) s.board[i] = buf.readInt();
|
||||
if (buf.readBoolean()) s.player1 = buf.readUuid();
|
||||
if (buf.readBoolean()) s.player2 = buf.readUuid();
|
||||
s.currentTurn = buf.readInt();
|
||||
s.winner = buf.readInt();
|
||||
s.isSpectator = buf.readBoolean();
|
||||
return s;
|
||||
}
|
||||
|
||||
public static class State {
|
||||
public int[] board;
|
||||
public UUID player1, player2;
|
||||
public int currentTurn, winner;
|
||||
public boolean isSpectator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeNbt(NbtCompound nbt) {
|
||||
super.writeNbt(nbt);
|
||||
nbt.putIntArray("Board", board);
|
||||
if (player1 != null) nbt.putUuid("Player1", player1);
|
||||
if (player2 != null) nbt.putUuid("Player2", player2);
|
||||
nbt.putInt("Turn", currentTurn);
|
||||
nbt.putInt("Winner", winner);
|
||||
nbt.putInt("ResetTimer", resetTimer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readNbt(NbtCompound nbt) {
|
||||
super.readNbt(nbt);
|
||||
int[] saved = nbt.getIntArray("Board");
|
||||
if (saved.length == 9) board = saved;
|
||||
if (nbt.containsUuid("Player1")) player1 = nbt.getUuid("Player1");
|
||||
if (nbt.containsUuid("Player2")) player2 = nbt.getUuid("Player2");
|
||||
currentTurn = nbt.getInt("Turn");
|
||||
winner = nbt.getInt("Winner");
|
||||
resetTimer = nbt.getInt("ResetTimer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NbtCompound toInitialChunkDataNbt() { return createNbt(); }
|
||||
|
||||
@Override
|
||||
public BlockEntityUpdateS2CPacket toUpdatePacket() {
|
||||
return BlockEntityUpdateS2CPacket.create(this);
|
||||
}
|
||||
|
||||
public void resetGame(net.minecraft.server.MinecraftServer server) {
|
||||
closeScreenForAll(server); // kick everyone from screen first
|
||||
if (player1 != null) Szar.tttActivePlayers.remove(player1);
|
||||
if (player2 != null) Szar.tttActivePlayers.remove(player2);
|
||||
spectators.clear();
|
||||
board = new int[9];
|
||||
player1 = null;
|
||||
player2 = null;
|
||||
currentTurn = 1;
|
||||
winner = 0;
|
||||
resetTimer = -1;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
public void closeScreenForAll(net.minecraft.server.MinecraftServer server) {
|
||||
closeScreenForPlayer(server, player1);
|
||||
closeScreenForPlayer(server, player2);
|
||||
for (UUID uuid : spectators) {
|
||||
closeScreenForPlayer(server, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeScreenForPlayer(net.minecraft.server.MinecraftServer server, UUID uuid) {
|
||||
if (uuid == null) return;
|
||||
ServerPlayerEntity p = server.getPlayerManager().getPlayer(uuid);
|
||||
if (p == null) return;
|
||||
ServerPlayNetworking.send(p, Szar.TTT_CLOSE_SCREEN, PacketByteBufs.empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"variants": {
|
||||
"": { "model": "szar:block/tictactoe" }
|
||||
}
|
||||
}
|
||||
@@ -185,5 +185,7 @@
|
||||
"advancement.szar.dontknow.description": "Ask a question from Merl",
|
||||
|
||||
"advancement.szar.backrooms.title": "Where did the world go?",
|
||||
"advancement.szar.backrooms.description": "Step into the Backrooms"
|
||||
"advancement.szar.backrooms.description": "Step into the Backrooms",
|
||||
|
||||
"block.szar.tictactoe": "Tic Tac Toe"
|
||||
}
|
||||
|
||||
24
src/main/resources/assets/szar/models/block/tictactoe.json
Normal file
24
src/main/resources/assets/szar/models/block/tictactoe.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"parent": "block/block",
|
||||
"format_version": "1.9.0",
|
||||
"credit": "Made with Blockbench",
|
||||
"textures": {
|
||||
"0": "szar:block/tictactoe",
|
||||
"1": "szar:block/plank",
|
||||
"particle": "szar:block/tictactoe"
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"from": [0, 0, 0],
|
||||
"to": [16, 12, 16],
|
||||
"faces": {
|
||||
"north": {"uv": [0, 0, 16, 12], "texture": "#1"},
|
||||
"east": {"uv": [0, 0, 16, 12], "texture": "#1"},
|
||||
"south": {"uv": [0, 0, 16, 12], "texture": "#1"},
|
||||
"west": {"uv": [0, 0, 16, 12], "texture": "#1"},
|
||||
"up": {"uv": [0, 0, 16, 16], "texture": "#0"},
|
||||
"down": {"uv": [0, 0, 16, 16], "texture": "#1"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"parent": "szar:block/tictactoe"
|
||||
}
|
||||
BIN
src/main/resources/assets/szar/textures/block/plank.png
Normal file
BIN
src/main/resources/assets/szar/textures/block/plank.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 405 B |
BIN
src/main/resources/assets/szar/textures/block/tictactoe.png
Normal file
BIN
src/main/resources/assets/szar/textures/block/tictactoe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 579 B |
BIN
src/main/resources/assets/szar/textures/gui/o.png
Normal file
BIN
src/main/resources/assets/szar/textures/gui/o.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 754 B |
BIN
src/main/resources/assets/szar/textures/gui/tictactoe.png
Normal file
BIN
src/main/resources/assets/szar/textures/gui/tictactoe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/main/resources/assets/szar/textures/gui/x.png
Normal file
BIN
src/main/resources/assets/szar/textures/gui/x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 794 B |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"values": [
|
||||
"szar:roulette",
|
||||
"szar:slot_machine"
|
||||
"szar:slot_machine",
|
||||
"szar:tictactoe"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "szar:tictactoe"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
23
src/main/resources/data/szar/recipes/tictactoe.json
Normal file
23
src/main/resources/data/szar/recipes/tictactoe.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"pattern": [
|
||||
"RBR",
|
||||
"BPB",
|
||||
"RBR"
|
||||
],
|
||||
"key": {
|
||||
"R": {
|
||||
"item": "minecraft:red_dye"
|
||||
},
|
||||
"B": {
|
||||
"item": "minecraft:blue_dye"
|
||||
},
|
||||
"P": {
|
||||
"tag": "minecraft:planks"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"item": "szar:tictactoe",
|
||||
"count": 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user