Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ object ConnectedTankGameTest {
* @param tankPos タンクを置きたい相対座標 (y >= 2)
* @param tier 設置するタンクのティア (デフォルト BASE)
*/
private fun GameTestHelper.placeTank(tankPos: BlockPos, tier: TankTier = TankTier.BASE) {
private fun GameTestHelper.placeTank(tankPos: BlockPos, tier: TankTier = TankTier.BASE, sneaking: Boolean = false) {
val basePos = tankPos.below()
setBlock(basePos, Blocks.STONE)
val player = makeMockPlayer(GameType.SURVIVAL)
Expand All @@ -45,6 +45,7 @@ object ConnectedTankGameTest {
}
val stack = ItemStack(item)
player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND, stack)
if (sneaking) player.setShiftKeyDown(true)
placeAt(player, stack, basePos.below(), Direction.UP)
}

Expand Down Expand Up @@ -1193,4 +1194,111 @@ object ConnectedTankGameTest {
)
context.succeed()
}

// === スニーク設置分離テスト ===

//? if fabric {
@GameTest
//?}
fun sneakingPlacementOnNonTankCreatesIsolatedGroup(context: GameTestHelper) {
val pos1 = BlockPos(0, 2, 0)
val pos2 = BlockPos(1, 2, 0)
context.placeTank(pos1)
context.placeTank(pos2, sneaking = true)

val state = context.getFluidState()
val storage1 = state.getStorage(context.absolutePos(pos1))
val storage2 = state.getStorage(context.absolutePos(pos2))
context.assertTrue(storage1 != null, Component.literal("Storage1 should exist"))
context.assertTrue(storage2 != null, Component.literal("Storage2 should exist"))
context.assertTrue(
storage1 !== storage2,
Component.literal("Sneaking placement should create isolated group"),
)
context.assertTrue(
storage1!!.bucketCapacity == CTServerConfig.DEFAULT_BUCKET_CAPACITY,
Component.literal("First tank should have single capacity"),
)
context.assertTrue(
storage2!!.bucketCapacity == CTServerConfig.DEFAULT_BUCKET_CAPACITY,
Component.literal("Second tank should have single capacity"),
)

val blockState1 = context.level.getBlockState(context.absolutePos(pos1))
val blockState2 = context.level.getBlockState(context.absolutePos(pos2))
context.assertTrue(
!blockState1.getValue(ConnectedTankBlock.CONNECTED_EAST),
Component.literal("pos1 CONNECTED_EAST should be false"),
)
context.assertTrue(
!blockState2.getValue(ConnectedTankBlock.CONNECTED_WEST),
Component.literal("pos2 CONNECTED_WEST should be false"),
)
context.succeed()
}

//? if fabric {
@GameTest
//?}
fun normalPlacementNextToTankConnects(context: GameTestHelper) {
val pos1 = BlockPos(0, 2, 0)
val pos2 = BlockPos(1, 2, 0)
context.placeTank(pos1)
context.placeTank(pos2, sneaking = false)

val state = context.getFluidState()
val storage1 = state.getStorage(context.absolutePos(pos1))
val storage2 = state.getStorage(context.absolutePos(pos2))
context.assertTrue(storage1 != null, Component.literal("Storage1 should exist"))
context.assertTrue(storage2 != null, Component.literal("Storage2 should exist"))
context.assertTrue(
storage1 === storage2,
Component.literal("Normal placement should connect to adjacent tank"),
)
context.succeed()
}

//? if fabric {
@GameTest
//?}
fun sneakingPlacementBetweenTwoGroupsStaysIsolated(context: GameTestHelper) {
val posA = BlockPos(0, 2, 0)
val posB = BlockPos(2, 2, 0)
val posMid = BlockPos(1, 2, 0)
context.placeTank(posA)
context.placeTank(posB)
context.placeTank(posMid, sneaking = true)

val state = context.getFluidState()
val sA = state.getStorage(context.absolutePos(posA))
val sMid = state.getStorage(context.absolutePos(posMid))
val sB = state.getStorage(context.absolutePos(posB))
context.assertTrue(sA !== sMid, Component.literal("Sneaking mid should not connect to A"))
context.assertTrue(sB !== sMid, Component.literal("Sneaking mid should not connect to B"))
context.assertTrue(sA !== sB, Component.literal("A and B should remain separate"))
context.succeed()
}

//? if fabric {
@GameTest
//?}
fun sneakingDoesNotLeakToNextPlacement(context: GameTestHelper) {
val posA = BlockPos(0, 2, 0)
val posB = BlockPos(1, 2, 0)
val posC = BlockPos(2, 2, 0)
context.placeTank(posA)
context.placeTank(posB, sneaking = true)
context.placeTank(posC, sneaking = false)

val state = context.getFluidState()
val sA = state.getStorage(context.absolutePos(posA))
val sB = state.getStorage(context.absolutePos(posB))
val sC = state.getStorage(context.absolutePos(posC))
context.assertTrue(sA !== sB, Component.literal("B should be isolated from A"))
context.assertTrue(
sB === sC,
Component.literal("C (normal) should connect to B, not stay isolated from stale sneaking"),
)
context.succeed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class BlockItemPlaceMixin {
hitPos = context.getClickedPos().relative(context.getClickedFace().getOpposite());
}
ConnectedTankPlacementContext.INSTANCE.setInteractedAt(hitPos);
var player = context.getPlayer();
ConnectedTankPlacementContext.INSTANCE.setSneaking(player != null && player.isSecondaryUseActive());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,12 @@ class ConnectedTankBlock(val tier: TankTier, settings: Properties) :
val interactedAt = ConnectedTankPlacementContext.consumeInteractedAt()?.takeIf {
CTBlocks.isConnectedTank(world.getBlockState(it).block)
}
persistentState.addStorage(pos, tankStorage, interactedAt)
val sneaking = ConnectedTankPlacementContext.consumeSneaking()
if (sneaking && interactedAt == null) {
persistentState.addIsolatedStorage(pos, tankStorage)
} else {
persistentState.addStorage(pos, tankStorage, interactedAt)
}
CTBlocks.syncGroupBlockEntities(world, pos, persistentState)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ import net.minecraft.core.BlockPos

object ConnectedTankPlacementContext {
private val interactedAt = ThreadLocal<BlockPos?>()
private val sneaking = ThreadLocal<Boolean>()

fun setInteractedAt(pos: BlockPos) {
interactedAt.set(pos)
}

fun setSneaking(value: Boolean) {
sneaking.set(value)
}

/**
* interactedAt を取得して ThreadLocal から削除する。
* Mixin の RETURN inject が例外で到達しない場合、この呼び出しが唯一のクリーンアップ手段となる。
*/
fun consumeInteractedAt(): BlockPos? = interactedAt.get()?.also { interactedAt.remove() }

fun consumeSneaking(): Boolean = (sneaking.get() ?: false).also { sneaking.remove() }

fun clear() {
interactedAt.remove()
sneaking.remove()
}
}
Loading