Skip to content

Commit 4d81ac3

Browse files
authored
Merge pull request #58 from KJP12/fix/heads-creative
Fixes for textured heads & creative inventories
2 parents 250a8c2 + 388b826 commit 4d81ac3

File tree

5 files changed

+404
-71
lines changed

5 files changed

+404
-71
lines changed

src/main/java/org/samo_lego/golfiv/casts/ItemStackChecker.java

Lines changed: 140 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
import net.minecraft.entity.effect.StatusEffectInstance;
66
import net.minecraft.entity.effect.StatusEffects;
77
import net.minecraft.item.*;
8-
import net.minecraft.nbt.NbtCompound;
9-
import net.minecraft.nbt.NbtElement;
10-
import net.minecraft.nbt.NbtList;
8+
import net.minecraft.nbt.*;
119
import net.minecraft.potion.PotionUtil;
1210
import net.minecraft.util.Identifier;
1311
import net.minecraft.util.registry.Registry;
@@ -34,11 +32,11 @@ public interface ItemStackChecker {
3432
/**
3533
* Creates a fake ItemStack based on the
3634
* provided stack.
37-
*
35+
* <p>
3836
* E.g. removes enchantment info, stack size, etc.
3937
* Used on ground ItemEntities / opponent's stacks etc.
4038
*
41-
* @param original the original ItemStack
39+
* @param original the original ItemStack
4240
* @param spoofCount whether or not the item count should be faked
4341
* @return the faked ItemStack
4442
*/
@@ -47,52 +45,78 @@ static ItemStack fakeStack(ItemStack original, boolean spoofCount) {
4745
new ItemStack(original.getItem(), original.getMaxCount()) :
4846
new ItemStack(original.getItem(), original.getCount());
4947

50-
if(original.hasEnchantments())
48+
if (original.hasEnchantments())
5149
fakedStack.addEnchantment(null, 0);
5250

5351
Item item = original.getItem();
54-
if(item instanceof DyeableItem dyeable) {
55-
if(dyeable.hasColor(original)) {
52+
if (item instanceof DyeableItem dyeable) {
53+
if (dyeable.hasColor(original)) {
5654
dyeable.setColor(fakedStack, dyeable.getColor(original));
5755
}
5856
}
5957

60-
if(item instanceof PotionItem || item instanceof TippedArrowItem) {
58+
if (item instanceof PotionItem || item instanceof TippedArrowItem) {
6159
// Lets dropping potions and arrows to be less of a 'surprising' change.
62-
fakedStack.getOrCreateNbt().putInt("CustomPotionColor", PotionUtil.getColor(original));
60+
fakedStack.setSubNbt(PotionUtil.CUSTOM_POTION_COLOR_KEY, NbtInt.of(PotionUtil.getColor(original)));
6361
if (item.hasGlint(original)) {
6462
PotionUtil.setCustomPotionEffects(fakedStack, UNLUCK);
6563
}
6664
}
6765

68-
if(item instanceof WritableBookItem || item instanceof WrittenBookItem) {
66+
if (item instanceof WritableBookItem || item instanceof WrittenBookItem) {
6967
// Prevents issues with other mods expecting pages to be present.
7068
fakedStack.setSubNbt("pages", new NbtList());
7169
}
7270

73-
if(item instanceof BannerItem) {
71+
if (item instanceof BannerItem) {
7472
// Lets dropping banners not be a surprising change, and for illager patrol leaders to have a proper banner.
75-
cleanBanner(original.getSubNbt("BlockEntityTag"), fakedStack);
73+
cleanBanner(original.getSubNbt(BlockItem.BLOCK_STATE_TAG_KEY), fakedStack);
74+
}
75+
76+
if (item instanceof SkullItem) {
77+
// Lets dropping player heads not be a surprising change, and for wearing heads to show the actual skin.
78+
cleanSkull(original.getSubNbt(SkullItem.SKULL_OWNER_KEY), fakedStack);
79+
}
80+
81+
if (item instanceof CrossbowItem && CrossbowItem.isCharged(original)) {
82+
cleanCrossbow(original.getNbt(), fakedStack, false);
7683
}
7784

7885
return fakedStack;
7986
}
8087

88+
/**
89+
* This is for keeping creative from getting inadvertently sanitised,
90+
* as creative has the property of allowing one to summon any item at will,
91+
* including freely mutating the inventory.
92+
*
93+
* @param stack The original ItemStack.
94+
* @return The faked ItemStack, with a GolfIV pointer injected.
95+
* @author KJP12
96+
* @see #inventoryStack(ItemStack)
97+
*/
98+
static ItemStack creativeInventoryStack(ItemStack stack) {
99+
ItemStack fake = inventoryStack(stack);
100+
if (stack.hasNbt()) //noinspection ConstantConditions
101+
fake.setSubNbt("GolfIV", NbtInt.of(stack.getNbt().hashCode()));
102+
return fake;
103+
}
104+
81105
/**
82106
* Creates a fake ItemStack based on the
83107
* provided stack.
84-
*
108+
* <p>
85109
* Changes the tag to be the minimal required
86110
* NBT to render in an inventory.
87111
*
88112
* @param stack The original ItemStack.
89113
* @return The faked ItemStack
90-
* */
114+
*/
91115
static ItemStack inventoryStack(ItemStack stack) {
92116
// TODO: Perhaps take a more dynamic approach to this?
93117
// This is not really flexible as is and may leave modded items broken.
94118
NbtCompound tag = stack.getNbt();
95-
if(tag == null || tag.isEmpty()) {
119+
if (tag == null || tag.isEmpty()) {
96120
// Sanitization isn't necessary when it's already empty.
97121
return stack;
98122
}
@@ -101,14 +125,15 @@ static ItemStack inventoryStack(ItemStack stack) {
101125
ItemStack fake = new ItemStack(item, stack.getCount());
102126

103127
// Rewrite display.
104-
if(tag.contains(ItemStack.DISPLAY_KEY)) {
128+
if (tag.contains(ItemStack.DISPLAY_KEY)) {
105129
NbtCompound display = tag.getCompound(ItemStack.DISPLAY_KEY);
106130
NbtCompound fakeDisplay = new NbtCompound();
107131
NbtElement name = display.get(ItemStack.NAME_KEY);
108-
if(name != null) fakeDisplay.put(ItemStack.NAME_KEY, name);
109-
if(display.contains(ItemStack.COLOR_KEY)) fakeDisplay.put(ItemStack.COLOR_KEY, display.get(ItemStack.COLOR_KEY));
132+
if (name != null) fakeDisplay.put(ItemStack.NAME_KEY, name);
133+
if (display.contains(ItemStack.COLOR_KEY))
134+
fakeDisplay.put(ItemStack.COLOR_KEY, display.get(ItemStack.COLOR_KEY));
110135
NbtElement lore = display.get(ItemStack.LORE_KEY);
111-
if(lore != null) fakeDisplay.put(ItemStack.LORE_KEY, lore);
136+
if (lore != null) fakeDisplay.put(ItemStack.LORE_KEY, lore);
112137
fake.setSubNbt(ItemStack.DISPLAY_KEY, fakeDisplay);
113138
}
114139

@@ -140,31 +165,40 @@ static ItemStack inventoryStack(ItemStack stack) {
140165
// Check block items.
141166
if(item instanceof BlockItem) {
142167
boolean flag = true;
143-
if(item instanceof BannerItem) {
168+
if (item instanceof BannerItem) {
144169
flag = false;
145-
cleanBanner(stack.getSubNbt("BlockEntityTag"), fake);
170+
cleanBanner(stack.getSubNbt(BlockItem.BLOCK_STATE_TAG_KEY), fake);
146171
}
147172

148-
if(flag) {
173+
// Transfer SkullProperties.
174+
if (item instanceof SkullItem) {
175+
flag = false;
176+
cleanSkull(stack.getSubNbt(SkullItem.SKULL_OWNER_KEY), fake);
177+
}
178+
179+
if (flag) {
149180
Block block = ((BlockItem) item).getBlock();
150181

151182
// Rewrite shulker items
152183
if (block instanceof ShulkerBoxBlock) {
153-
NbtCompound blockEntity = stack.getSubNbt("BlockEntityTag");
154-
if(blockEntity != null) {
155-
NbtCompound fakeEntity = fake.getOrCreateSubNbt("BlockEntityTag");
156-
if(blockEntity.contains("LootTable", NbtElement.STRING_TYPE)) {
184+
NbtCompound blockEntity = stack.getSubNbt(BlockItem.BLOCK_STATE_TAG_KEY);
185+
if (blockEntity != null) {
186+
NbtCompound fakeEntity = fake.getOrCreateSubNbt(BlockItem.BLOCK_STATE_TAG_KEY);
187+
if (blockEntity.contains("LootTable", NbtElement.STRING_TYPE)) {
157188
fakeEntity.put("LootTable", blockEntity.get("LootTable"));
158189
}
159-
if(blockEntity.contains("Items", NbtElement.LIST_TYPE)) {
190+
if (blockEntity.contains("Items", NbtElement.LIST_TYPE)) {
160191
NbtList fakeItems = new NbtList();
161-
for(NbtElement $item : blockEntity.getList("Items", NbtElement.COMPOUND_TYPE)) {
162-
if($item == null) continue;
192+
for (NbtElement $item : blockEntity.getList("Items", NbtElement.COMPOUND_TYPE)) {
193+
if ($item == null) continue;
163194
NbtCompound oldItem = (NbtCompound) $item;
164195
NbtCompound fakeItem = new NbtCompound();
165-
if(oldItem.contains("Slot", NbtElement.BYTE_TYPE)) fakeItem.put("Slot", oldItem.get("Slot"));
166-
if(oldItem.contains("id", NbtElement.STRING_TYPE)) fakeItem.put("id", oldItem.get("id"));
167-
if(oldItem.contains("Count", NbtElement.BYTE_TYPE)) fakeItem.put("Count", oldItem.get("Count"));
196+
if (oldItem.contains("Slot", NbtElement.BYTE_TYPE))
197+
fakeItem.put("Slot", oldItem.get("Slot"));
198+
if (oldItem.contains("id", NbtElement.STRING_TYPE))
199+
fakeItem.put("id", oldItem.get("id"));
200+
if (oldItem.contains("Count", NbtElement.BYTE_TYPE))
201+
fakeItem.put("Count", oldItem.get("Count"));
168202
// TODO: Add in display name
169203
fakeItems.add(fakeItem);
170204
}
@@ -204,27 +238,94 @@ static ItemStack inventoryStack(ItemStack stack) {
204238
fake.setSubNbt("pages", new NbtList());
205239
}
206240

207-
if(item instanceof WritableBookItem) {
241+
if (item instanceof WritableBookItem) {
208242
// FIXME: Pages need to be present for the book to work. Force update on selection?
209243
// Prevents issues with other mods expecting pages to be present.
210244
fake.setSubNbt("pages", new NbtList());
211245
}
212246

213-
if(item instanceof FilledMapItem) {
247+
if (item instanceof FilledMapItem) {
214248
fake.getOrCreateNbt().putInt("map", tag.getInt("map"));
215249
}
216250

251+
if (item instanceof CrossbowItem && CrossbowItem.isCharged(stack)) {
252+
cleanCrossbow(stack.getNbt(), fake, true);
253+
}
254+
217255
return fake;
218256
}
219257

220258
/**
221-
* Minimally copies over the banner NBT based on the provided blockEntity data.
259+
* Minimally copies over the crossbow data based on the provided Crossbow data.
260+
* <p>
261+
* Only the NBT required to render the crossbow, including rocket, is copied over.
262+
*
263+
* @param crossbow The raw crossbow NBT.
264+
* @param faked The faked ItemStack to copy to.
265+
* @param isInventory Whether to send the raw item or not.
266+
*/
267+
static void cleanCrossbow(NbtCompound crossbow, ItemStack faked, boolean isInventory) {
268+
if (crossbow == null) return;
269+
NbtList originalProjectiles = crossbow.getList("ChargedProjectiles", NbtElement.COMPOUND_TYPE);
270+
if (originalProjectiles.isEmpty()) return;
271+
String originalProjectile = originalProjectiles.getCompound(0).getString("id");
272+
String projectile;
273+
274+
if (isInventory || "minecraft:firework".equals(originalProjectile)) {
275+
projectile = originalProjectile;
276+
} else {
277+
projectile = "minecraft:arrow";
278+
}
279+
280+
NbtCompound projectileStack = new NbtCompound();
281+
projectileStack.putByte("Count", (byte) 1);
282+
projectileStack.putString("id", projectile);
283+
NbtList projectiles = new NbtList();
284+
projectiles.add(projectileStack);
285+
faked.setSubNbt("ChargedProjectiles", projectiles);
286+
faked.setSubNbt("Charged", NbtByte.of(true));
287+
}
288+
289+
/**
290+
* Minimally copies over the skull data based on the provided SkullOwner data.
291+
* <p>
292+
* Only the NBT required to render the skull is copied over.
222293
*
294+
* @param skullOwner The SkullOwner NBT compound. May be null.
295+
* @param faked The faked ItemStack to copy to.
296+
*/
297+
static void cleanSkull(NbtCompound skullOwner, ItemStack faked) {
298+
if (skullOwner != null) {
299+
NbtCompound fakeSkullOwner = faked.getOrCreateSubNbt(SkullItem.SKULL_OWNER_KEY);
300+
if (skullOwner.containsUuid("Id")) fakeSkullOwner.putUuid("Id", skullOwner.getUuid("Id"));
301+
if (skullOwner.contains("Properties", NbtElement.COMPOUND_TYPE)) {
302+
NbtCompound skullProperties = skullOwner.getCompound("Properties");
303+
if (skullProperties.contains("textures", NbtElement.LIST_TYPE)) {
304+
NbtList skullTextures = skullProperties.getList("textures", NbtElement.COMPOUND_TYPE);
305+
if (!skullTextures.isEmpty()) {
306+
NbtCompound skullValueContainer = skullTextures.getCompound(0);
307+
String skullTexture = skullValueContainer.getString("Value");
308+
NbtCompound fakeProperties = new NbtCompound();
309+
NbtList fakeTextures = new NbtList();
310+
NbtCompound fakeTexture = new NbtCompound();
311+
fakeTexture.putString("Value", skullTexture);
312+
fakeTextures.add(fakeTexture);
313+
fakeProperties.put("textures", fakeTextures);
314+
fakeSkullOwner.put("Properties", fakeProperties);
315+
}
316+
}
317+
}
318+
}
319+
}
320+
321+
/**
322+
* Minimally copies over the banner NBT based on the provided blockEntity data.
323+
* <p>
223324
* Only the NBT required to render a layer is copied over.
224325
*
225326
* @param blockEntity The original block entity tag. May be null.
226-
* @param faked The faked ItemStack to copy to.
227-
* */
327+
* @param faked The faked ItemStack to copy to.
328+
*/
228329
static void cleanBanner(NbtCompound blockEntity, ItemStack faked) {
229330
if (blockEntity != null && blockEntity.contains("Patterns", NbtElement.LIST_TYPE)) {
230331
NbtList fakePatterns = new NbtList();
@@ -247,7 +348,7 @@ static void cleanBanner(NbtCompound blockEntity, ItemStack faked) {
247348
}
248349
}
249350
}
250-
faked.getOrCreateSubNbt("BlockEntityTag").put("Patterns", fakePatterns);
351+
faked.getOrCreateSubNbt(BlockItem.BLOCK_STATE_TAG_KEY).put("Patterns", fakePatterns);
251352
}
252353
}
253354
}

src/main/java/org/samo_lego/golfiv/event/S2CPacket/ItemInventoryKickPatch.java

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,75 @@
1313
import org.samo_lego.golfiv.mixin.accessors.ScreenHandlerSlotUpdateS2CPacketAccessor;
1414

1515
import java.util.List;
16-
import java.util.stream.Collectors;
16+
import java.util.function.Consumer;
1717

1818
import static org.samo_lego.golfiv.GolfIV.golfConfig;
19-
import static org.samo_lego.golfiv.casts.ItemStackChecker.fakeStack;
19+
import static org.samo_lego.golfiv.casts.ItemStackChecker.creativeInventoryStack;
20+
import static org.samo_lego.golfiv.casts.ItemStackChecker.inventoryStack;
2021

2122
public class ItemInventoryKickPatch implements S2CPacketCallback {
2223

2324
public ItemInventoryKickPatch() {
2425
}
2526

2627
/**
27-
* Changes ItemStacks in {@link InventoryS2CPacket}
28-
* to not include tags, as they get sent additionally by {@link ScreenHandlerSlotUpdateS2CPacket}
28+
* Removes non-critical tags from item stacks in the event of a
29+
* creative player and packet overflows.
30+
* <p>
31+
* Creative players will additionally also get a {@code GolfIV} tag
32+
* injected as a hash of the original NBT to prevent Creative inventory
33+
* management from unexpectedly mangling the item either by just opening
34+
* the inventory or by {@link org.samo_lego.golfiv.storage.GolfConfig.IllegalItems.Creative#removeCreativeNBTTags
35+
* Remove Creative NBT Tags}.
2936
*
3037
* @param packet packet being sent
3138
* @param player player getting the packet
3239
* @param server Minecraft Server
40+
* @see ItemStackChecker#fakeStack(ItemStack, boolean)
41+
* @see ItemStackChecker#inventoryStack(ItemStack)
42+
* @see org.samo_lego.golfiv.mixin.illegal_items.ServerPlayNetworkHandlerMixin_CreativeItemsCheck
3343
*/
3444
@Override
3545
public void preSendPacket(Packet<?> packet, ServerPlayerEntity player, MinecraftServer server) {
36-
if(golfConfig.packet.patchItemKickExploit && packet instanceof InventoryS2CPacket) {
37-
PacketByteBuf testBuf = new PacketByteBuf(Unpooled.buffer());
38-
packet.write(testBuf);
39-
if(testBuf.readableBytes() > 2097140) {
40-
List<ItemStack> contents = ((InventoryS2CPacketAccessor) packet).getContents();
41-
List<ItemStack> fakedContents = contents.stream().map(ItemStackChecker::inventoryStack).collect(Collectors.toList());
46+
if (!golfConfig.packet.patchItemKickExploit) return;
47+
if (packet instanceof InventoryS2CPacket inventoryPacket) {
48+
// The creative player does some weirdness in regards to sending packets.
49+
// This will try to guarantee that the creative player will not accidentally
50+
// wipe their inventory of various NBT required for the item.
51+
52+
// For the moment, this may produce some weirdness in regards to rendering as this doesn't quite
53+
// send all possible NBT. However, the inventory will remain intact, which arguably is better
54+
// than some missing data for the client.
55+
if (player.isCreative() || isOversized(packet::write)) {
56+
List<ItemStack> contents = inventoryPacket.getContents();
57+
List<ItemStack> fakedContents = contents.stream().map(player.isCreative() ?
58+
ItemStackChecker::creativeInventoryStack :
59+
ItemStackChecker::inventoryStack)
60+
.toList();
4261

4362
((InventoryS2CPacketAccessor) packet).setContents(fakedContents);
4463
}
45-
testBuf.release();
46-
} else if(golfConfig.packet.patchItemKickExploit && packet instanceof ScreenHandlerSlotUpdateS2CPacket) {
64+
} else if (packet instanceof ScreenHandlerSlotUpdateS2CPacket) {
4765
ItemStack stack = ((ScreenHandlerSlotUpdateS2CPacketAccessor) packet).getStack();
48-
if(stack.getNbt() != null) {
49-
PacketByteBuf testBuf = new PacketByteBuf(Unpooled.buffer());
50-
if(testBuf.writeItemStack(stack).readableBytes() > 2097140) {
51-
((ScreenHandlerSlotUpdateS2CPacketAccessor) packet).setStack(fakeStack(stack, false));
52-
}
53-
testBuf.release();
66+
if (stack.hasNbt() && (player.isCreative() || isOversized(buf -> buf.writeItemStack(stack)))) {
67+
((ScreenHandlerSlotUpdateS2CPacketAccessor) packet).setStack(player.isCreative() ? creativeInventoryStack(stack) : inventoryStack(stack));
5468
}
5569
}
5670
}
71+
72+
/**
73+
* Tests if the packet will overflow the maximum allowed for the buffer.
74+
*
75+
* @param packet The packet in the form of a consumer. Allows for arbitrary packets.
76+
* @return true if the packet is greater than 2,097,140, false otherwise.
77+
*/
78+
private static boolean isOversized(Consumer<PacketByteBuf> packet) {
79+
PacketByteBuf testBuf = new PacketByteBuf(Unpooled.buffer());
80+
try {
81+
packet.accept(testBuf);
82+
return testBuf.readableBytes() > 2097140;
83+
} finally {
84+
testBuf.release();
85+
}
86+
}
5787
}

0 commit comments

Comments
 (0)