Skip to content

Commit 0a119a2

Browse files
committed
Fix several crash/robustness issues in packet handling, line wrapping, and fluid GUI
PlayerPacketHandler: translate copies of item stacks and rebuild outgoing packets instead of mutating shared NMS ItemStacks in place. The handler runs on Netty's I/O thread while the same stacks may be touched by the main thread; mutating their component maps races and can corrupt packets (observed as ArrayIndexOutOfBoundsException / "Can't find id for 'null'" on 1.21.6+). Also wrap packet rewriting in try/catch so a failure falls back to sending the original packet rather than dropping the connection. LineWrapping: guard against an infinite loop / StringIndexOutOfBounds when wrapping text that has no spaces (e.g. CJK languages) or when a single token is longer than the wrap limit. Handles empty content, non-positive remaining width, and clamps the cut index to the content length. FluidButton: make currentFluid nullable and resolve it via getOrNull so an empty/0-size fluids list can no longer throw IndexOutOfBounds; fall back to a shared error item and bail out of click handling when no fluid is available. FluidIntersectionMarker: resolve the connected pipe defensively (pipeOrNull). When the held intersection display or its pipe display entity has been lost, getWaila now hides the WAILA and getPickItem returns null instead of NPE'ing the WAILA update tick.
1 parent 3a6a406 commit 0a119a2

4 files changed

Lines changed: 97 additions & 37 deletions

File tree

nms/src/main/kotlin/io/github/pylonmc/rebar/nms/packet/PlayerPacketHandler.kt

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import io.github.pylonmc.rebar.culling.BlockCullingEngine
77
import io.github.pylonmc.rebar.i18n.PlayerTranslationHandler
88
import io.github.pylonmc.rebar.nms.entity.BlockTextureEntityImpl
99
import io.github.pylonmc.rebar.util.position.BlockPosition
10+
import com.mojang.datafixers.util.Pair
1011
import io.netty.channel.ChannelDuplexHandler
1112
import io.netty.channel.ChannelHandlerContext
1213
import io.netty.channel.ChannelPromise
14+
import net.minecraft.core.NonNullList
1315
import net.minecraft.network.HashedPatchMap
1416
import net.minecraft.network.HashedStack
1517
import net.minecraft.network.protocol.Packet
@@ -57,28 +59,58 @@ class PlayerPacketHandler(private val player: ServerPlayer, val handler: PlayerT
5759
private inner class PacketHandler : ChannelDuplexHandler() {
5860
override fun write(ctx: ChannelHandlerContext, packet: Any, promise: ChannelPromise) {
5961
@Suppress("UNCHECKED_CAST")
60-
val packet = packet as? Packet<in ClientGamePacketListener> ?: return super.write(ctx, packet, promise)
61-
super.write(ctx, handleOutgoingPacket(packet), promise)
62+
val pkt = packet as? Packet<in ClientGamePacketListener>
63+
if (pkt == null) {
64+
super.write(ctx, packet, promise)
65+
return
66+
}
67+
68+
try {
69+
super.write(ctx, handleOutgoingPacket(pkt), promise)
70+
} catch (e: Exception) {
71+
Rebar.logger.log(Level.WARNING, "Failed to rewrite outgoing packet, sending original", e)
72+
super.write(ctx, pkt, promise)
73+
}
6274
}
6375

6476
override fun channelRead(ctx: ChannelHandlerContext, packet: Any) {
6577
@Suppress("UNCHECKED_CAST")
66-
val packet = packet as? Packet<in ServerGamePacketListener> ?: return super.channelRead(ctx, packet)
67-
super.channelRead(ctx, handleIncomingPacket(packet))
78+
val pkt = packet as? Packet<in ServerGamePacketListener>
79+
if (pkt == null) {
80+
super.channelRead(ctx, packet)
81+
return
82+
}
83+
84+
try {
85+
super.channelRead(ctx, handleIncomingPacket(pkt))
86+
} catch (e: Exception) {
87+
Rebar.logger.log(Level.WARNING, "Failed to rewrite incoming packet, forwarding original", e)
88+
super.channelRead(ctx, pkt)
89+
}
6890
}
6991
}
7092

7193
private fun handleOutgoingPacket(packet: Packet<in ClientGamePacketListener>): Packet<in ClientGamePacketListener> =
7294
when (packet) {
7395
is ClientboundBundlePacket -> ClientboundBundlePacket(packet.subPackets().map(::handleOutgoingPacket))
7496

75-
is ClientboundContainerSetContentPacket -> packet.apply {
76-
items.forEach(::translate)
77-
translate(packet.carriedItem)
78-
}
97+
// Translate copies and rebuild the packet, never modify inventory/equipment stacks in place.
98+
// These stacks may be shared with server-side state while this handler runs on Netty's I/O
99+
// thread, and mutating their component maps can corrupt packets on 1.21.6+ servers.
100+
is ClientboundContainerSetContentPacket -> ClientboundContainerSetContentPacket(
101+
packet.containerId,
102+
packet.stateId,
103+
NonNullList.of(ItemStack.EMPTY, *packet.items.map { translate(it.copy()) }.toTypedArray()),
104+
translate(packet.carriedItem.copy())
105+
)
79106

80-
is ClientboundContainerSetSlotPacket -> packet.apply { translate(item) }
81-
is ClientboundSetCursorItemPacket -> packet.apply { translate(contents) }
107+
is ClientboundContainerSetSlotPacket -> ClientboundContainerSetSlotPacket(
108+
packet.containerId,
109+
packet.stateId,
110+
packet.slot,
111+
translate(packet.item.copy())
112+
)
113+
is ClientboundSetCursorItemPacket -> ClientboundSetCursorItemPacket(translate(packet.contents.copy()))
82114
is ClientboundRecipeBookAddPacket -> ClientboundRecipeBookAddPacket(
83115
packet.entries.map {
84116
ClientboundRecipeBookAddPacket.Entry(
@@ -136,11 +168,10 @@ class PlayerPacketHandler(private val player: ServerPlayer, val handler: PlayerT
136168
)
137169
}
138170

139-
is ClientboundSetEquipmentPacket -> packet.apply {
140-
slots.forEach { slot ->
141-
translate(slot.second)
142-
}
143-
}
171+
is ClientboundSetEquipmentPacket -> ClientboundSetEquipmentPacket(
172+
packet.entity,
173+
packet.slots.map { Pair.of(it.first, translate(it.second.copy())) }
174+
)
144175

145176
is ClientboundBlockUpdatePacket -> packet.let {
146177
val cullingJob = BlockCullingEngine.getCullingJob(player.uuid) ?: return@let it

rebar/src/main/kotlin/io/github/pylonmc/rebar/content/fluid/FluidIntersectionMarker.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,27 @@ class FluidIntersectionMarker : RebarBlock, EntityHolderRebarBlock, BlockBreakRe
4949
}
5050
}
5151

52-
override fun getWaila(player: Player): WailaDisplay?
53-
= WailaDisplay(defaultWailaTranslationKey.arguments(RebarArgument.of("pipe", this.pipe.stack.effectiveName())))
52+
override fun getWaila(player: Player): WailaDisplay? {
53+
val pipeItem = pipeOrNull() ?: return null
54+
return WailaDisplay(defaultWailaTranslationKey.arguments(RebarArgument.of("pipe", pipeItem.stack.effectiveName())))
55+
}
5456

5557
val pipe: RebarItem
56-
get() {
57-
check(fluidIntersectionDisplay.connectedPipeDisplays.isNotEmpty())
58-
val uuid = fluidIntersectionDisplay.connectedPipeDisplays.iterator().next()
59-
return EntityStorage.getAs<FluidPipeDisplay?>(uuid)!!.pipe
58+
get() = pipeOrNull() ?: error("FluidIntersectionMarker has no resolvable pipe display")
59+
60+
private fun pipeOrNull(): RebarItem? {
61+
val display = try {
62+
fluidIntersectionDisplay
63+
} catch (_: Throwable) {
64+
return null
6065
}
66+
val uuid = display.connectedPipeDisplays.firstOrNull() ?: return null
67+
return EntityStorage.getAs<FluidPipeDisplay?>(uuid)?.pipe
68+
}
6169

6270
override fun getDropItem(context: BlockBreakContext) = null
6371

64-
override fun getPickItem(player: Player) = pipe.stack
72+
override fun getPickItem(player: Player) = pipeOrNull()?.stack
6573

6674
companion object {
6775
@JvmField

rebar/src/main/kotlin/io/github/pylonmc/rebar/guide/button/FluidButton.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import org.bukkit.entity.Player
1515
import org.bukkit.event.inventory.ClickType
1616
import xyz.xenondevs.invui.Click
1717
import xyz.xenondevs.invui.item.AbstractItem
18+
import xyz.xenondevs.invui.item.ItemProvider
1819

1920
/**
2021
* Represents a fluid in the guide.
@@ -52,44 +53,49 @@ open class FluidButton(
5253
constructor(input: RecipeInput.Fluid) : this(input.amountMillibuckets, *input.fluids.toTypedArray())
5354

5455
val fluids = fluids.shuffled()
55-
val currentFluid: RebarFluid
56-
get() = this.fluids[(Bukkit.getCurrentTick() / 20) % this.fluids.size]
56+
val currentFluid: RebarFluid?
57+
get() = this.fluids.getOrNull((Bukkit.getCurrentTick() / 20) % this.fluids.size)
5758

5859
init {
5960
require(fluids.isNotEmpty()) { "Fluids list cannot be empty" }
6061
}
6162

6263
override fun getUpdatePeriod(what: Int): Int = if (fluids.size > 1) 20 else -1
6364

64-
override fun getItemProvider(player: Player) = try {
65-
val stack = if (amount == null) {
66-
preDisplayDecorator.invoke(ItemStackBuilder.of(currentFluid.item))
65+
override fun getItemProvider(player: Player): ItemProvider = try {
66+
val fluid = currentFluid ?: return buildErrorItem()
67+
val itemStack = fluid.item
68+
69+
if (amount == null) {
70+
preDisplayDecorator.invoke(ItemStackBuilder.of(itemStack))
6771
} else {
68-
preDisplayDecorator.invoke(ItemStackBuilder.of(currentFluid.item))
72+
preDisplayDecorator.invoke(ItemStackBuilder.of(itemStack))
6973
.name(
7074
Component.translatable(
7175
"rebar.guide.button.fluid.name",
72-
RebarArgument.of("fluid", currentFluid.item.getData(DataComponentTypes.ITEM_NAME)!!),
76+
RebarArgument.of("fluid", itemStack.getData(DataComponentTypes.ITEM_NAME)!!),
7377
RebarArgument.of("amount", UnitFormat.MILLIBUCKETS.format(amount).decimalPlaces(2))
7478
)
7579
)
7680
}
77-
stack
7881
} catch (e: Exception) {
7982
e.printStackTrace()
80-
ItemStackBuilder.of(Material.BARRIER)
81-
.name(Component.translatable("rebar.guide.button.fluid.error"))
83+
buildErrorItem()
8284
}
8385

86+
private fun buildErrorItem() = ItemStackBuilder.of(Material.BARRIER)
87+
.name(Component.translatable("rebar.guide.button.fluid.error"))
88+
8489
override fun handleClick(clickType: ClickType, player: Player, click: Click) {
8590
try {
91+
val fluid = currentFluid ?: return
8692
if (clickType.isLeftClick) {
87-
val page = FluidRecipesPage(currentFluid.key)
93+
val page = FluidRecipesPage(fluid.key)
8894
if (page.pages.isNotEmpty()) {
8995
page.open(player)
9096
}
9197
} else {
92-
val page = FluidUsagesPage(currentFluid)
98+
val page = FluidUsagesPage(fluid)
9399
if (page.pages.isNotEmpty()) {
94100
page.open(player)
95101
}

rebar/src/main/kotlin/io/github/pylonmc/rebar/i18n/LineWrapping.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,33 @@ private fun wrapLine(
9090
// which are handled clientside) but this is fine, it can't be perfect.
9191
var content = component.content()
9292
while (currentLineLength + content.length > RebarConfig.TRANSLATION_WRAP_LIMIT) {
93+
if (content.isEmpty()) {
94+
break
95+
}
9396

9497
// Make sure we snap to the end of a word (i.e. don't cut words in half)
95-
var endIndex = RebarConfig.TRANSLATION_WRAP_LIMIT - currentLineLength
96-
while (endIndex != 0 && content[endIndex] != ' ') {
98+
val maxCharsOnLine = RebarConfig.TRANSLATION_WRAP_LIMIT - currentLineLength
99+
if (maxCharsOnLine <= 0) {
100+
lines.add(currentLine)
101+
currentLine = DEFAULT_COMPONENT
102+
currentLineLength = 0
103+
continue
104+
}
105+
106+
var endIndex = maxCharsOnLine.coerceAtMost(content.length)
107+
while (endIndex > 0 && endIndex < content.length && content[endIndex] != ' ') {
97108
endIndex -= 1
98109
}
99110

111+
if (endIndex <= 0) {
112+
endIndex = maxCharsOnLine.coerceAtMost(content.length)
113+
}
114+
100115
currentLine = currentLine.append(Component.text(content.substring(0, endIndex)).style(style))
101116
lines.add(currentLine)
102117
currentLine = DEFAULT_COMPONENT
103118
currentLineLength = 0
104-
content = if (endIndex+1 == content.length) "" else content.substring(endIndex+1)
119+
content = if (endIndex >= content.length) "" else content.substring(endIndex + 1)
105120
}
106121
currentLine = currentLine.append(Component.text(content).style(style))
107122
currentLineLength += content.length

0 commit comments

Comments
 (0)