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
104 changes: 78 additions & 26 deletions integration/mineflayer/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,64 @@ function waitFor(predicate, description, timeout = timeoutMs) {
})
}

async function rightClickBlock(target) {
const swingArm = bot.swingArm
bot.swingArm = () => {}
try {
const westFace = target.position.offset(-1, 0, 0).minus(target.position)
const westFaceCenter = target.position.offset(0, 0.5, 0.5).minus(target.position)
await bot.activateBlock(target, westFace, westFaceCenter)
} finally {
bot.swingArm = swingArm
}
}

async function exerciseFreezeLifecycle(target, expectedBlockName) {
const packetOffset = entityPackets.length
await rightClickBlock(target)

const displays = await waitFor(() => {
const packets = entityPackets.slice(packetOffset)
const itemDisplay = packets.find(packet =>
packet.name.includes('spawn_entity') && packet.type === bot.registry.entitiesByName.item_display.id
)
const blockDisplay = packets.find(packet =>
packet.name.includes('spawn_entity') && packet.type === bot.registry.entitiesByName.block_display.id
)
return itemDisplay && blockDisplay ? { itemDisplay, blockDisplay } : null
}, 'VirtualEntities Item Display and Block Display')

await bot.waitForTicks(5)
await bot.lookAt(target.position.offset(0.5, 0.5, 0.5), true)
await rightClickBlock(target)

await waitFor(
() => {
const destroyPackets = entityPackets.slice(packetOffset)
.filter(packet => packet.name === 'entity_destroy')
const removed = entityId => destroyPackets.some(packet =>
Array.from(packet.entityIds ?? []).includes(entityId)
)
return removed(displays.itemDisplay.entityId) && removed(displays.blockDisplay.entityId)
},
'VirtualEntities removal'
)

await bot.waitForTicks(10)
const displayEntityIds = new Set(entityPackets.slice(packetOffset)
.filter(packet =>
packet.name.includes('spawn_entity') &&
(packet.type === bot.registry.entitiesByName.item_display.id ||
packet.type === bot.registry.entitiesByName.block_display.id)
)
.map(packet => packet.entityId))
assert.equal(displayEntityIds.size, 2, 'unfreezing must not freeze a block behind the target')
await waitFor(
() => bot.blockAt(target.position)?.name === expectedBlockName,
`${expectedBlockName} block restoration`
)
}

try {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out waiting for Mineflayer spawn')), timeoutMs)
Expand All @@ -67,17 +125,24 @@ try {
bot.once('kicked', reason => reject(new Error(`Mineflayer was kicked: ${reason}`)))
})

bot.setSettings({ locale: 'zh_TW' })
await bot.waitForTicks(10)

bot.chat('/dsp give')
const debugStick = await waitFor(
() => bot.inventory.items().find(item => item.name === 'blaze_rod'),
'DebugStickPro item'
)
await waitFor(
() => messages.some(message => message.includes('已將除錯棒給予')),
'Traditional Chinese locale response'
)
await bot.equip(debugStick, 'hand')
await bot.waitForTicks(20)

bot.chat('/dsp mode freeze')
await waitFor(
() => messages.some(message => /freeze/i.test(message)),
() => messages.some(message => /freeze|凍結/i.test(message)),
'freeze mode confirmation'
)
await bot.waitForTicks(10)
Expand All @@ -92,38 +157,25 @@ try {
await bot.waitForTicks(10)
await bot.lookAt(target.position.offset(0.5, 0.5, 0.5), true)

const packetOffset = entityPackets.length
await bot.activateBlock(target)

const displays = await waitFor(() => {
const packets = entityPackets.slice(packetOffset)
const itemDisplay = packets.find(packet =>
packet.name.includes('spawn_entity') && packet.type === bot.registry.entitiesByName.item_display.id
)
const blockDisplay = packets.find(packet =>
packet.name.includes('spawn_entity') && packet.type === bot.registry.entitiesByName.block_display.id
)
return itemDisplay && blockDisplay ? { itemDisplay, blockDisplay } : null
}, 'VirtualEntities Item Display and Block Display')
await exerciseFreezeLifecycle(target, 'stone')

await waitFor(
() => {
const destroyPackets = entityPackets.slice(packetOffset)
.filter(packet => packet.name === 'entity_destroy')
const removed = entityId => destroyPackets.some(packet =>
Array.from(packet.entityIds ?? []).includes(entityId)
)
return removed(displays.itemDisplay.entityId) && removed(displays.blockDisplay.entityId)
},
'VirtualEntities removal'
)
bot.chat(`/setblock ${targetPosition.x} ${targetPosition.y} ${targetPosition.z} minecraft:barrier`)
const barrier = await waitFor(() => {
const block = bot.blockAt(targetPosition)
return block?.name === 'barrier' ? block : null
}, 'integration-test barrier block')
await bot.waitForTicks(10)
await bot.lookAt(barrier.position.offset(0.5, 0.5, 0.5), true)
await exerciseFreezeLifecycle(barrier, 'barrier')

console.log(JSON.stringify({
version,
locale: 'zh_TW',
command: true,
miniMessageItem: debugStick.customName?.toString() ?? debugStick.displayName,
virtualEntities: ['item_display', 'block_display'],
removal: true
removal: true,
barrierRestoration: true
}))
} finally {
bot.quit('DebugStickPro E2E complete')
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/dev/twme/debugstickpro/DebugStickPro.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import dev.twme.debugstickpro.listeners.WorldUnloadEventListener;
import dev.twme.debugstickpro.localization.LangFileManager;
import dev.twme.debugstickpro.localization.PlayerLanguageManager;
import dev.twme.debugstickpro.localization.PlayerLocaleResolver;
import dev.twme.debugstickpro.mode.freeze.FreezeBlockManager;
import dev.twme.debugstickpro.mode.freeze.FreezePacketLayer;
import dev.twme.debugstickpro.playerdata.PlayerData;
Expand Down Expand Up @@ -154,7 +155,7 @@ public void onDisable() {
*/
public void onServerReloadCommand() {
for (Player player : Bukkit.getOnlinePlayers()) {
PlayerLanguageManager.setPlayerLocale(player.getUniqueId(), player.getLocale());
PlayerLanguageManager.setPlayerLocale(player.getUniqueId(), PlayerLocaleResolver.resolve(player));

UUID playerUUID = player.getUniqueId();
PlayerDataManager.setPlayerData(playerUUID, new PlayerData());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.twme.debugstickpro.listeners;

import dev.twme.debugstickpro.localization.PlayerLanguageManager;
import dev.twme.debugstickpro.localization.PlayerLocaleResolver;
import dev.twme.debugstickpro.playerdata.PlayerData;
import dev.twme.debugstickpro.playerdata.PlayerDataManager;
import dev.twme.debugstickpro.utils.DebugStickItem;
Expand All @@ -17,7 +18,7 @@ public class PlayerJoinListener implements Listener {
public void onPlayerJoinEvent(PlayerJoinEvent event) {
Player player = event.getPlayer();

PlayerLanguageManager.setPlayerLocale(player.getUniqueId(), player.getLocale());
PlayerLanguageManager.setPlayerLocale(player.getUniqueId(), PlayerLocaleResolver.resolve(player));

UUID playerUUID = event.getPlayer().getUniqueId();
PlayerDataManager.setPlayerData(playerUUID, new PlayerData());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package dev.twme.debugstickpro.listeners;

import dev.twme.debugstickpro.localization.PlayerLanguageManager;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerLocaleChangeEvent;

public class PlayerLocaleChangeEventListener implements Listener {
@EventHandler
public void onPlayerLocaleChangeEvent(PlayerLocaleChangeEvent event) {
Player player = event.getPlayer();
PlayerLanguageManager.setPlayerLocale(player.getUniqueId(), player.getLocale());
PlayerLanguageManager.setPlayerLocale(event.getPlayer().getUniqueId(), event.getLocale());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.twme.debugstickpro.listeners;

import dev.twme.debugstickpro.localization.PlayerLanguageManager;
import dev.twme.debugstickpro.mode.freeze.FreezeBlockManager;
import dev.twme.debugstickpro.playerdata.PlayerDataManager;
import org.bukkit.event.EventHandler;
Expand All @@ -14,6 +15,7 @@ public void onPlayerQuitEvent(PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
PlayerDataManager.removePlayerFromDisplayList(uuid);
PlayerDataManager.removePlayerData(uuid);
PlayerLanguageManager.removePlayerLocale(uuid);
FreezeBlockManager.removeAllPlayerFrozenBlock(uuid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dev.twme.debugstickpro.config.ConfigFile;

import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

Expand Down Expand Up @@ -33,6 +34,26 @@ public static String getLocale(UUID playerUUID) {
* @param locale the locale of the player
*/
public static void setPlayerLocale(UUID playerUUID, String locale) {
playerLang.put(playerUUID, locale);
playerLang.put(playerUUID, normalizeLocale(locale));
}

public static void removePlayerLocale(UUID playerUUID) {
playerLang.remove(playerUUID);
}

public static String normalizeLocale(String locale) {
if (locale == null || locale.isBlank()) {
return ConfigFile.Language.DefaultLanguage;
}

String[] parts = locale.replace('-', '_').split("_", 3);
StringBuilder normalized = new StringBuilder(parts[0].toLowerCase(Locale.ROOT));
if (parts.length >= 2 && !parts[1].isBlank()) {
normalized.append('_').append(parts[1].toUpperCase(Locale.ROOT));
}
if (parts.length == 3 && !parts[2].isBlank()) {
normalized.append('_').append(parts[2]);
}
return normalized.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.twme.debugstickpro.localization;

import org.bukkit.entity.Player;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;

public final class PlayerLocaleResolver {
private PlayerLocaleResolver() {
}

public static String resolve(Player player) {
try {
Method localeMethod = Player.class.getMethod("locale");
Object locale = localeMethod.invoke(player);
if (locale instanceof Locale paperLocale) {
return paperLocale.toString();
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) {
// Spigot does not expose Paper's Locale-returning API.
}

return player.getLocale();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public static void removeOneBlock(UUID playerUUID, Block block) {

freezeBlockLocations.remove(freezeLocation);
freezeBlocks.remove(frozenData);
FreezePacketLayer.resyncBlock(block);
if (freezeBlocks.isEmpty()) {
playerFrozenBlockData.remove(playerUUID);
}
Expand All @@ -117,6 +118,7 @@ public static void removeAllPlayerFrozenBlock(UUID playerUUID) {
removeDisplayEntity(frozenData.getItemDisplay());
removeDisplayEntity(frozenData.getBlockDisplay());
freezeBlockLocations.remove(new FreezeLocation(block.getLocation()));
FreezePacketLayer.resyncBlock(block);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMultiBlockChange;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;

public final class FreezePacketLayer {
Expand Down Expand Up @@ -43,6 +44,22 @@ public static synchronized void shutdown() {
initialized = false;
}

public static void resyncBlock(Block block) {
var playerManager = PacketEvents.getAPI().getPlayerManager();
Vector3i position = new Vector3i(block.getX(), block.getY(), block.getZ());
String blockData = block.getBlockData().getAsString();

for (Player player : block.getWorld().getPlayers()) {
WrappedBlockState state = WrappedBlockState.getByString(
playerManager.getClientVersion(player),
blockData
);
if (state != null) {
playerManager.sendPacket(player, new WrapperPlayServerBlockChange(position, state));
}
}
}

private static final class FreezePacketListener extends SimplePacketListenerAbstract {
private FreezePacketListener() {
super(PacketListenerPriority.NORMAL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,17 @@ public static void onRightClick(UUID playerUUID, Action action, Block clickedBlo
}

private static Block resolveTargetBlock(Player player, Action action, Block clickedBlock, BlockFace clickedFace) {
if (action == Action.RIGHT_CLICK_BLOCK && clickedBlock != null) {
if (FreezeBlockManager.isFreezeBlock(clickedBlock.getLocation())) {
return clickedBlock;
}
Block frozenClickedTarget = resolveFrozenClickedTarget(clickedBlock, clickedFace);
if (frozenClickedTarget != null) {
return frozenClickedTarget;
}

Block frozenSightTarget = resolveFrozenSightTarget(player, clickedBlock);
if (frozenSightTarget != null) {
return frozenSightTarget;
}

if (action == Action.RIGHT_CLICK_BLOCK && clickedBlock != null) {
Block supportBlock = resolveFrozenSupportBlock(clickedBlock, clickedFace);
if (supportBlock != null) {
return supportBlock;
Expand All @@ -72,6 +78,35 @@ private static Block resolveTargetBlock(Player player, Action action, Block clic
return null;
}

private static Block resolveFrozenClickedTarget(Block clickedBlock, BlockFace clickedFace) {
if (clickedBlock == null) {
return null;
}
if (FreezeBlockManager.isFreezeBlock(clickedBlock.getLocation())) {
return clickedBlock;
}
if (clickedFace == null || clickedFace == BlockFace.SELF) {
return null;
}

Block blockInFront = clickedBlock.getRelative(clickedFace);
return FreezeBlockManager.isFreezeBlock(blockInFront.getLocation()) ? blockInFront : null;
}

private static Block resolveFrozenSightTarget(Player player, Block clickedBlock) {
BlockIterator sightLine = new BlockIterator(player, TARGET_DISTANCE);
while (sightLine.hasNext()) {
Block block = sightLine.next();
if (FreezeBlockManager.isFreezeBlock(block.getLocation())) {
return block;
}
if (clickedBlock != null && clickedBlock.equals(block)) {
return null;
}
}
return null;
}

private static Block resolveAirTarget(Player player) {
Block normalTarget = player.getTargetBlockExact(TARGET_DISTANCE);
// BlockIterator also visits air cells, so display-only frozen blocks remain targetable.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.twme.debugstickpro.localization;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class PlayerLanguageManagerTest {
@Test
public void normalizesPaperLanguageTag() {
assertEquals("zh_TW", PlayerLanguageManager.normalizeLocale("zh-TW"));
}

@Test
public void normalizesSpigotLocale() {
assertEquals("en_US", PlayerLanguageManager.normalizeLocale("EN_us"));
}

@Test
public void preservesLocaleVariant() {
assertEquals("zh_HANT_TW", PlayerLanguageManager.normalizeLocale("zh-Hant-TW"));
}
}
Loading