Initial commit

This commit is contained in:
ShadowNetReal
2026-07-23 18:21:37 -04:00
commit a35a0892f6
81 changed files with 613418 additions and 0 deletions
@@ -0,0 +1,32 @@
package tf.tuff;
import com.github.retrooper.packetevents.event.PacketListener;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData;
import org.bukkit.World;
import org.bukkit.entity.Player;
public class NetworkListener implements PacketListener {
private final TuffX plugin;
public NetworkListener(TuffX plugin) {
this.plugin = plugin;
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.CHUNK_DATA) {
Player player = (Player) event.getPlayer();
if (player == null) return;
WrapperPlayServerChunkData wrapper = new WrapperPlayServerChunkData(event);
int chunkX = wrapper.getColumn().getX();
int chunkZ = wrapper.getColumn().getZ();
World world = player.getWorld();
plugin.y0Plugin.chunkPacketListener.handleChunk(plugin, player, world, chunkX, chunkZ);
}
}
}
+68
View File
@@ -0,0 +1,68 @@
package tf.tuff;
import org.bukkit.plugin.java.JavaPlugin;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import tf.tuff.util.SchedulerCompat;
public class ServerRegistry {
private final JavaPlugin p;
private final String wsUrl;
private final String server;
private WebSocketClient client;
private volatile boolean running = true;
public ServerRegistry(JavaPlugin pl, String registryUrl, String serverAddr) {
p = pl;
wsUrl = registryUrl;
server = serverAddr;
}
public void connect() {
SchedulerCompat.runAsync(p, this::doConnect);
}
private void doConnect() {
if (!running)
return;
try {
client = new WebSocketClient(new URI(wsUrl)) {
@Override
public void onOpen(ServerHandshake h) {
send("{\"type\":\"register\",\"server\":\"" + server + "\"}");
}
@Override
public void onMessage(String msg) {
}
@Override
public void onClose(int code, String reason, boolean remote) {
if (running) {
SchedulerCompat.runAsyncLater(p, ServerRegistry.this::doConnect, 100L);
}
}
@Override
public void onError(Exception e) {
}
};
client.setConnectionLostTimeout(30);
client.connect();
} catch (Exception e) {
if (running) {
SchedulerCompat.runAsyncLater(p, this::doConnect, 100L);
}
}
}
public void disconnect() {
running = false;
if (client != null) {
client.close();
}
}
}
+354
View File
@@ -0,0 +1,354 @@
package tf.tuff;
import java.io.File;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.*;
import org.bukkit.event.entity.EntityToggleSwimEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.java.JavaPluginLoader;
import org.bukkit.plugin.messaging.PluginMessageListener;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import tf.tuff.netty.ChunkInjector;
import tf.tuff.tuffactions.TuffActions;
import tf.tuff.util.SchedulerCompat;
import tf.tuff.viablocks.ViaBlocksPlugin;
import tf.tuff.viaentities.ViaEntitiesPlugin;
import tf.tuff.y0.Y0Plugin;
public class TuffX extends JavaPlugin implements Listener, PluginMessageListener {
public ServerRegistry serverRegistry;
public Y0Plugin y0Plugin;
public ViaBlocksPlugin viaBlocksPlugin;
public TuffActions tuffActions;
public ViaEntitiesPlugin viaEntitiesPlugin;
private ChunkInjector chunkInjector;
private boolean packetEventsEnabled;
// required by MockBukkit
public TuffX(JavaPluginLoader loader, PluginDescriptionFile description, File dataFolder, File file) {
super(loader, description, dataFolder, file);
}
public TuffX() { super(); }
@Override
public void onLoad() {
this.y0Plugin = new Y0Plugin(this);
this.viaBlocksPlugin = new ViaBlocksPlugin(this);
this.tuffActions = new TuffActions(this);
this.viaEntitiesPlugin = new ViaEntitiesPlugin(this);
if (shouldBootstrapPacketEvents()) {
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this));
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
.checkForUpdates(false);
PacketEvents.getAPI().load();
packetEventsEnabled = true;
}
}
@Override
public void onEnable() {
if (packetEventsEnabled && PacketEvents.getAPI() != null) {
PacketEvents.getAPI().init();
} else {
packetEventsEnabled = false;
}
saveDefaultConfig();
getLogger().info(SchedulerCompat.isFolia()
? "Folia detected. Using region and entity schedulers."
: "Using standard Bukkit-compatible schedulers.");
y0Plugin.onTuffXEnable();
tuffActions.onTuffXEnable();
viaBlocksPlugin.onTuffXEnable();
viaEntitiesPlugin.onTuffXEnable();
chunkInjector = new ChunkInjector(viaBlocksPlugin.blockListener, y0Plugin);
viaBlocksPlugin.blockListener.setChunkInjector(chunkInjector);
y0Plugin.setChunkInjector(chunkInjector);
getConfig().options().copyDefaults(true);
saveConfig();
getServer().getScheduler().runTaskTimer(this, () -> {
Runtime runtime = Runtime.getRuntime();
long used = runtime.totalMemory() - runtime.freeMemory();
long max = runtime.maxMemory();
double percent = (double) used / (double) max;
if (percent >= 0.80D) {
if (y0Plugin != null) {
y0Plugin.forceClearCache();
}
getLogger().warning("[TuffXPlus] Memory watchdog cleared Y0 cache. Used: " + (used / 1024L / 1024L) + "MB / " + (max / 1024L / 1024L) + "MB");
}
if (percent >= 0.95D) {
System.gc();
getLogger().warning("[TuffXPlus] Emergency GC requested.");
}
}, 20L * 60L, 20L * 60L);
if (packetEventsEnabled) {
PacketEvents.getAPI().getEventManager().registerListener(
new NetworkListener(this), PacketListenerPriority.NORMAL
);
}
getServer().getPluginManager().registerEvents(this, this);
setupRegistry();
lfe();
}
private void setupRegistry() {
if (getConfig().getBoolean("registry.enabled", false)) {
String url = getConfig().getString("registry.server-url");
String ws = getConfig().getString("registry.server");
if (ws != null && !ws.isEmpty() && !ws.equals("wss://urserverip.net")) {
serverRegistry = new ServerRegistry(this, url, ws);
serverRegistry.connect();
}
}
}
@Override
public void onDisable() {
y0Plugin.onTuffXDisable();
viaBlocksPlugin.onTuffXDisable();
viaEntitiesPlugin.onTuffXDisable();
if (serverRegistry != null) {
serverRegistry.disconnect();
serverRegistry = null;
}
if (packetEventsEnabled && PacketEvents.getAPI() != null) {
PacketEvents.getAPI().terminate();
}
packetEventsEnabled = false;
getServer().getMessenger().unregisterIncomingPluginChannel(this);
getServer().getMessenger().unregisterOutgoingPluginChannel(this);
}
private boolean shouldBootstrapPacketEvents() {
return getServer() == null
|| !getServer().getClass().getName().startsWith("be.seeseemelk.mockbukkit");
}
public void reloadTuffX(){
saveDefaultConfig();
reloadConfig();
getConfig().options().copyDefaults(true);
saveConfig();
if (serverRegistry != null) {
serverRegistry.disconnect();
serverRegistry = null;
}
setupRegistry();
viaBlocksPlugin.onTuffXReload();
y0Plugin.onTuffXReload();
tuffActions.onTuffXReload();
viaEntitiesPlugin.onTuffXReload();
getLogger().info("TuffX reloaded.");
}
public boolean TuffXCommand(CommandSender sender, Command command, String label, String[] args){
if (args.length > 0) {
if (args[0].equalsIgnoreCase("reload")) {
if (!(sender instanceof Player)) {
reloadTuffX();
} else {
Player player = (Player) sender;
if (!player.hasPermission("tuffx.reload")) {
player.sendMessage("§cYou do not have permission to use this command.");
return false;
}
reloadTuffX();
player.sendMessage("TuffX reloaded.");
}
}
}
return true;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equalsIgnoreCase("tuffx")) return TuffXCommand(sender, command, label, args);
if (command.getName().equalsIgnoreCase("viablocks")) return viaBlocksPlugin.onTuffXCommand(sender, command, label, args);
if (command.getName().equalsIgnoreCase("restrictions")) return tuffActions.onTuffXCommand(sender, command, label, args);
if (command.getName().equalsIgnoreCase("tuffxclearcache")) {
if (!sender.hasPermission("tuffx.admin")) {
sender.sendMessage("§cNo permission.");
return true;
}
if (y0Plugin != null) {
y0Plugin.forceClearCache();
}
Runtime runtime = Runtime.getRuntime();
long used = runtime.totalMemory() - runtime.freeMemory();
long max = runtime.maxMemory();
sender.sendMessage("§aTuffXPlus Y0 cache cleared.");
sender.sendMessage("§7Used RAM: §f" + (used / 1024L / 1024L) + "MB / " + (max / 1024L / 1024L) + "MB");
return true;
}
return true;
}
@Override
public void onPluginMessageReceived(String channel, Player player, byte[] message) {
if (!player.isOnline()) return;
if (channel.equals("eagler:below_y0")) y0Plugin.handlePacket(player,message);
else if (channel.equals("viablocks:handshake")) viaBlocksPlugin.handlePacket(player,message);
else if (channel.equals("eagler:tuffactions")) tuffActions.handlePacket(player,message);
else if (channel.equals("entities:handshake")) viaEntitiesPlugin.handlePacket(player,message);
else getLogger().warning("Received plugin message on unknown channel '%s' from %s".formatted(channel, player.getName()));
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangeWorld(PlayerChangedWorldEvent e) {
y0Plugin.handlePlayerChangeWorld(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockForm(BlockFormEvent e) {
viaBlocksPlugin.blockListener.handleBlockForm(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockFade(BlockFadeEvent e) {
viaBlocksPlugin.blockListener.handleBlockFade(e);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent e) {
y0Plugin.handlePlayerJoin(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockGrow(BlockGrowEvent e) {
viaBlocksPlugin.blockListener.handleBlockGrow(e);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
y0Plugin.handlePlayerQuit(e);
tuffActions.handlePlayerQuit(e);
viaBlocksPlugin.blockListener.handlePlayerQuit(e);
viaEntitiesPlugin.handlePlayerQuit(e);
}
@EventHandler
public void onToggleSwim(EntityToggleSwimEvent e) {
tuffActions.handleToggleSwim(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockSpread(BlockSpreadEvent e) {
viaBlocksPlugin.blockListener.handleBlockSpread(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent e) {
viaBlocksPlugin.blockListener.handleBlockBreak(e);
y0Plugin.handleBlockBreak(e);
}
@EventHandler
public void onPlayerInventoryClick(InventoryClickEvent e) {
tuffActions.handlePlayerInventoryClick(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockPlace(BlockPlaceEvent e) {
viaBlocksPlugin.blockListener.handleBlockPlace(e);
y0Plugin.handleBlockPlace(e);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockPhysics(BlockPhysicsEvent e) {
y0Plugin.handleBlockPhysics(e);
viaBlocksPlugin.blockListener.handleBlockPhysics(e);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onChunkLoad(ChunkLoadEvent e) {
y0Plugin.handleChunkLoad(e);
viaBlocksPlugin.blockListener.handleChunkLoad(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockExplode(BlockExplodeEvent e) {
viaBlocksPlugin.blockListener.handleBlockExplode(e);
y0Plugin.handleBlockExplode(e);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockFromTo(BlockFromToEvent e) {
viaBlocksPlugin.blockListener.handleBlockFromTo(e);
y0Plugin.handleBlockFromTo(e);
}
private void lfe() {
getLogger().info("");
getLogger().info("████████╗██╗ ██╗███████╗ ███████╗ ██╗ ██╗ ██╗");
getLogger().info("╚══██╔══╝██║ ██║██╔════╝ ██╔════╝ ╚██╗██╔╝ ██║");
getLogger().info(" ██║ ██║ ██║██████╗ ██████╗ ╚███╔╝ ████████████╗");
getLogger().info(" ██║ ██║ ██║██╔═══╝ ██╔═══╝ ██╔██╗ ╚════██╔════╝");
getLogger().info(" ██║ ╚██████╔╝██║ ██║ ██╔╝╚██╗ ██║");
getLogger().info(" ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝");
getLogger().info("");
getLogger().info("CREDITS");
getLogger().info("");
getLogger().info("Y0 support:");
getLogger().info("• Below y0 (client + plugin) programmed by Potato (@justatypicalpotato)");
getLogger().info("• llucasandersen - plugin optimizations");
getLogger().info("");
getLogger().info("ViaBlocks:");
getLogger().info("• ViaBlocks partial plugin and client rewrite by Potato");
getLogger().info("• llucasandersen (Complex client models and texture fixes,");
getLogger().info(" optimizations, PacketEvents migration and async safety fixes)");
getLogger().info("• coleis1op, if ts is driving me crazy, im taking credit");
getLogger().info("");
getLogger().info("Other:");
getLogger().info("• Swimming and creative items programmed by Potato (@justatypicalpotato)");
getLogger().info("• shaded build, 1.14+ support (before merge) - llucasandersen");
getLogger().info("• Restrictions - UplandJacob");
getLogger().info("• Overall plugin merges by Potato");
getLogger().info("• Major Ram optimizations - MrNorshare");
}
}
@@ -0,0 +1,77 @@
package tf.tuff.netty;
import com.viaversion.viaversion.api.Via;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import org.bukkit.entity.Player;
import java.util.UUID;
public abstract class BaseInjector {
private final String handlerName;
protected BaseInjector(String handlerName) {
this.handlerName = handlerName;
}
protected abstract ChannelHandler createHandler(Player player);
protected void onPostInject(Player player) {
}
public void inject(Player player) {
UUID uuid = player.getUniqueId();
var viaConnection = Via.getAPI().getConnection(uuid);
if (viaConnection == null) return;
Channel channel = viaConnection.getChannel();
if (channel == null) return;
channel.eventLoop().submit(() -> {
try {
if (channel.pipeline().get(handlerName) != null) {
channel.pipeline().remove(handlerName);
}
String targetHandler = null;
String[] anchors = {"packet_handler", "encoder", "via-encoder"};
for (int i = 0; i < anchors.length; ++i) {
if (channel.pipeline().get(anchors[i]) != null) {
targetHandler = anchors[i];
break;
}
}
ChannelHandler handler = createHandler(player);
if (targetHandler != null) {
channel.pipeline().addBefore(targetHandler, handlerName, handler);
} else {
channel.pipeline().addFirst(handlerName, handler);
}
onPostInject(player);
} catch (Exception e) {
e.printStackTrace();
}
});
}
public void eject(Player player) {
UUID uuid = player.getUniqueId();
var viaConnection = Via.getAPI().getConnection(uuid);
if (viaConnection == null) return;
Channel channel = viaConnection.getChannel();
if (channel != null && channel.isOpen()) {
channel.eventLoop().submit(() -> {
try {
if (channel.pipeline().get(handlerName) != null) {
channel.pipeline().remove(handlerName);
}
} catch (Exception e) {
}
});
}
}
}
@@ -0,0 +1,455 @@
package tf.tuff.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import org.bukkit.World;
import org.bukkit.entity.Player;
import tf.tuff.viablocks.CustomBlockListener;
import tf.tuff.y0.Y0Plugin;
import tf.tuff.util.SchedulerCompat;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class ChunkHandler extends ChannelOutboundHandlerAdapter {
private final CustomBlockListener viaBlocks;
private final Y0Plugin y0;
private final Player player;
private final UUID playerId;
private final Map<Long, QueuedPacket> queue = new ConcurrentHashMap<>();
private volatile ChannelHandlerContext ctx;
private static final long TIMEOUT_MS = 500;
static record BlockChangePosition(int x, int y, int z) {}
public ChunkHandler(CustomBlockListener viaBlocks, Y0Plugin y0, Player player) {
this.viaBlocks = viaBlocks;
this.y0 = y0;
this.player = player;
this.playerId = player.getUniqueId();
}
private static class QueuedPacket {
final ByteBuf buf;
final ChannelPromise promise;
final int chunkX;
final int chunkZ;
final long time;
volatile boolean viaReady;
volatile boolean y0Ready;
volatile byte[] viaData;
volatile byte[] y0Data;
QueuedPacket(ByteBuf buf, ChannelPromise promise, int cx, int cz) {
this.buf = buf;
this.promise = promise;
this.chunkX = cx;
this.chunkZ = cz;
this.time = System.currentTimeMillis();
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof ByteBuf)) {
super.write(ctx, msg, promise);
return;
}
ByteBuf buf = (ByteBuf) msg;
buf.markReaderIndex();
try {
int packetId = readVarInt(buf);
if (packetId == 0x20) {
handleChunkPacket(ctx, buf, promise);
return;
}
if (packetId == 0x0B && isViaActive()) {
handleBlockChange(ctx, buf, promise);
return;
}
if (packetId == 0x10 && isViaActive()) {
handleMultiBlockChange(ctx, buf, promise);
return;
}
} catch (Exception e) {
} finally {
if (buf.refCnt() > 0 && msg == buf) {
buf.resetReaderIndex();
}
}
super.write(ctx, msg, promise);
}
private boolean isViaActive() {
return viaBlocks != null
&& viaBlocks.plugin.isEnabled()
&& viaBlocks.plugin.isPlayerEnabled(player);
}
private void handleChunkPacket(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) throws Exception {
int chunkX = buf.readInt();
int chunkZ = buf.readInt();
buf.resetReaderIndex();
boolean viaActive = isViaActive();
byte[] viaData = viaActive ? viaBlocks.getExtraDataForChunk(player.getWorld().getName(), chunkX, chunkZ) : null;
byte[] y0Data = y0 != null ? y0.getY0DataForChunk(player, chunkX, chunkZ) : null;
boolean needY0 = y0 != null && y0.isPlayerReady(player);
boolean viaReady = !viaActive || viaData != null;
boolean y0Ready = !needY0 || y0Data != null;
if (viaReady && y0Ready) {
writeWithData(ctx, buf, promise, viaData, y0Data);
return;
}
long key = key(chunkX, chunkZ);
QueuedPacket q = new QueuedPacket(buf.retain(), promise, chunkX, chunkZ);
q.viaReady = viaReady;
q.y0Ready = y0Ready;
q.viaData = viaData;
q.y0Data = y0Data;
QueuedPacket old = queue.put(key, q);
if (queue.size() > 256) {
clearOldQueuePackets(1000L);
}
if (queue.size() > 1024) {
forceClearQueue();
}
if (old != null) {
if (old.buf.refCnt() > 0) {
old.buf.release();
}
if (!old.promise.isDone()) {
old.promise.tryFailure(new RuntimeException("Replaced queued chunk packet"));
}
}
if (!viaReady) {
requestViaCache(chunkX, chunkZ, key);
}
if (!y0Ready) {
requestY0Cache(chunkX, chunkZ, key);
}
scheduleTimeout(key);
}
private void handleBlockChange(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) throws Exception {
BlockChangePosition position = decodeSingleBlockChangePosition(buf.getLong(buf.readerIndex()));
resolveViaDataOnRegionThread(ctx, buf, promise, player.getWorld(), position.x >> 4, position.z >> 4, () -> {
World world = player.getWorld();
if (!world.isChunkLoaded(position.x >> 4, position.z >> 4)) {
return null;
}
return viaBlocks.getExtraDataForSingleBlock(world, position.x, position.y, position.z);
});
}
private void handleMultiBlockChange(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) throws Exception {
buf.resetReaderIndex();
buf.skipBytes(varIntLen(buf));
long chunkSectionPos = buf.readLong();
int cx = decodeSectionCoordX(chunkSectionPos);
int cz = decodeSectionCoordZ(chunkSectionPos);
buf.readBoolean();
int count = readVarInt(buf);
java.util.List<Long> locs = new java.util.ArrayList<>(count);
for (int i = 0; i < count; i++) {
BlockChangePosition position = decodeMultiBlockChangePosition(chunkSectionPos, readVarLong(buf));
locs.add(viaBlocks.packLocation(position.x, position.y, position.z));
}
resolveViaDataOnRegionThread(ctx, buf, promise, player.getWorld(), cx, cz, () -> viaBlocks.getExtraDataForMultiBlock(player.getWorld(), locs));
}
private void resolveViaDataOnRegionThread(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise,
World world, int chunkX, int chunkZ,
java.util.concurrent.Callable<byte[]> supplier) {
ByteBuf retained = buf.retain();
SchedulerCompat.runRegion(viaBlocks.plugin.plugin, world, chunkX, chunkZ, () -> {
byte[] data = null;
try {
if (player.isOnline() && isViaActive()) {
data = supplier.call();
}
} catch (Exception ignored) {
}
final byte[] resolvedData = data;
ChannelHandlerContext currentCtx = this.ctx != null ? this.ctx : ctx;
currentCtx.channel().eventLoop().execute(() -> {
try {
retained.resetReaderIndex();
if (resolvedData != null && resolvedData.length > 0) {
writeWithViaOnly(currentCtx, retained, promise, resolvedData);
} else {
currentCtx.write(retained, promise);
}
} catch (Exception ignored) {
} finally {
if (retained.refCnt() > 0) {
retained.release();
}
}
});
});
}
private void requestViaCache(int cx, int cz, long key) {
SchedulerCompat.runRegion(viaBlocks.plugin.plugin, player.getWorld(), cx, cz, () -> {
if (!player.isOnline()) {
release(key);
return;
}
viaBlocks.cacheChunkWithCallback(player.getWorld(), cx, cz, data -> {
QueuedPacket q = queue.get(key);
if (q != null) {
q.viaData = data;
q.viaReady = true;
tryRelease(key);
}
});
});
}
private void requestY0Cache(int cx, int cz, long key) {
SchedulerCompat.runRegion(viaBlocks.plugin.plugin, player.getWorld(), cx, cz, () -> {
if (!player.isOnline()) {
release(key);
return;
}
y0.cacheChunkWithCallback(player, cx, cz, data -> {
QueuedPacket q = queue.get(key);
if (q != null) {
q.y0Data = data;
q.y0Ready = true;
tryRelease(key);
}
});
});
}
private void tryRelease(long key) {
QueuedPacket q = queue.get(key);
if (q != null && q.viaReady && q.y0Ready) {
release(key);
}
}
private void release(long key) {
QueuedPacket q = queue.remove(key);
if (q == null) return;
if (ctx != null && ctx.channel().isOpen()) {
ctx.channel().eventLoop().execute(() -> {
try {
writeWithData(ctx, q.buf, q.promise, q.viaData, q.y0Data);
} catch (Exception e) {
if (q.buf.refCnt() > 0) {
q.buf.release();
}
}
});
} else {
if (q.buf.refCnt() > 0) {
q.buf.release();
}
}
}
private void scheduleTimeout(long key) {
if (ctx != null) {
ctx.channel().eventLoop().schedule(() -> {
QueuedPacket q = queue.get(key);
if (q != null && System.currentTimeMillis() - q.time >= TIMEOUT_MS) {
release(key);
}
}, TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
}
private void writeWithData(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise,
byte[] viaData, byte[] y0Data) throws Exception {
boolean hasVia = viaData != null && viaData.length > 0;
boolean hasY0 = y0Data != null && y0Data.length > 0;
if (!hasVia && !hasY0) {
ctx.write(buf, promise);
return;
}
CompositeByteBuf composite = ctx.alloc().compositeBuffer();
composite.addComponent(true, buf);
if (hasVia) {
ByteBuf tail = ctx.alloc().buffer();
tail.writeBytes(viaData);
composite.addComponent(true, tail);
}
if (hasY0) {
ByteBuf tail = ctx.alloc().buffer();
tail.writeInt(0x59304348);
tail.writeInt(y0Data.length);
tail.writeBytes(y0Data);
composite.addComponent(true, tail);
}
ctx.write(composite, promise);
}
private void writeWithViaOnly(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise,
byte[] data) throws Exception {
ByteBuf tail = ctx.alloc().buffer();
tail.writeBytes(data);
CompositeByteBuf composite = ctx.alloc().compositeBuffer();
composite.addComponents(true, buf, tail);
ctx.write(composite, promise);
}
private long key(int x, int z) {
return ((long) x << 32) | (z & 0xFFFFFFFFL);
}
private int readVarInt(ByteBuf buf) {
int n = 0;
int r = 0;
byte b;
do {
b = buf.readByte();
r |= (b & 0x7F) << (7 * n++);
if (n > 5) throw new RuntimeException("VarInt too big");
} while ((b & 0x80) != 0);
return r;
}
private long readVarLong(ByteBuf buf) {
long value = 0L;
int position = 0;
byte currentByte;
do {
currentByte = buf.readByte();
value |= (long) (currentByte & 0x7F) << position;
position += 7;
if (position > 70) {
throw new RuntimeException("VarLong too big");
}
} while ((currentByte & 0x80) != 0);
return value;
}
static BlockChangePosition decodeSingleBlockChangePosition(long value) {
int x = decodeSigned((int) (value >> 38), 26);
int z = decodeSigned((int) ((value >> 12) & 0x3FFFFFFL), 26);
int y = decodeSigned((int) (value & 0xFFFL), 12);
return new BlockChangePosition(x, y, z);
}
static BlockChangePosition decodeMultiBlockChangePosition(long sectionPosition, long entry) {
int sectionX = decodeSectionCoordX(sectionPosition);
int sectionY = decodeSectionCoordY(sectionPosition);
int sectionZ = decodeSectionCoordZ(sectionPosition);
int localPosition = (int) (entry & 0xFFFL);
int x = (sectionX << 4) | ((localPosition >> 8) & 0xF);
int z = (sectionZ << 4) | ((localPosition >> 4) & 0xF);
int y = (sectionY << 4) | (localPosition & 0xF);
return new BlockChangePosition(x, y, z);
}
private static int decodeSectionCoordX(long sectionPosition) {
return decodeSigned((int) (sectionPosition >> 42), 22);
}
private static int decodeSectionCoordY(long sectionPosition) {
return decodeSigned((int) (sectionPosition & 0xFFFFFL), 20);
}
private static int decodeSectionCoordZ(long sectionPosition) {
return decodeSigned((int) ((sectionPosition >> 20) & 0x3FFFFFL), 22);
}
private static int decodeSigned(int value, int bits) {
int signBit = 1 << (bits - 1);
int fullMask = (1 << bits) - 1;
value &= fullMask;
return (value ^ signBit) - signBit;
}
private int varIntLen(ByteBuf buf) {
int s = buf.readerIndex();
readVarInt(buf);
int l = buf.readerIndex() - s;
buf.readerIndex(s);
return l;
}
public void forceClearQueue() {
for (QueuedPacket q : queue.values()) {
if (q.buf.refCnt() > 0) {
q.buf.release();
}
if (!q.promise.isDone()) {
q.promise.tryFailure(new RuntimeException("Force cleared queued chunk packet"));
}
}
queue.clear();
}
public void clearOldQueuePackets(long maxAgeMs) {
long now = System.currentTimeMillis();
for (Map.Entry<Long, QueuedPacket> entry : queue.entrySet()) {
QueuedPacket q = entry.getValue();
if (now - q.time >= maxAgeMs && queue.remove(entry.getKey(), q)) {
if (q.buf.refCnt() > 0) {
q.buf.release();
}
if (!q.promise.isDone()) {
q.promise.tryFailure(new RuntimeException("Expired queued chunk packet"));
}
}
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
for (QueuedPacket q : queue.values()) {
if (q.buf.refCnt() > 0) {
q.buf.release();
}
}
queue.clear();
}
}
@@ -0,0 +1,64 @@
package tf.tuff.netty;
import com.viaversion.viaversion.api.Via;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import org.bukkit.entity.Player;
import tf.tuff.viablocks.CustomBlockListener;
import tf.tuff.y0.Y0Plugin;
import java.util.UUID;
public class ChunkInjector extends BaseInjector {
private final CustomBlockListener viaBlocks;
private final Y0Plugin y0;
public ChunkInjector(CustomBlockListener viaBlocks, Y0Plugin y0) {
super("tuff_chunk_handler");
this.viaBlocks = viaBlocks;
this.y0 = y0;
}
@Override
protected ChannelHandler createHandler(Player player) {
return new ChunkHandler(viaBlocks, y0, player);
}
@Override
public void inject(Player player) {
UUID uuid = player.getUniqueId();
var viaConnection = Via.getAPI().getConnection(uuid);
if (viaConnection == null) return;
Channel channel = viaConnection.getChannel();
if (channel == null) return;
channel.eventLoop().submit(() -> {
try {
if (channel.pipeline().get("tuff_chunk_handler") != null) {
channel.pipeline().remove("tuff_chunk_handler");
}
if (channel.pipeline().get("viablocks_chunk_handler") != null) {
channel.pipeline().remove("viablocks_chunk_handler");
}
if (channel.pipeline().get("y0_chunk_handler") != null) {
channel.pipeline().remove("y0_chunk_handler");
}
if (channel.pipeline().get("via-encoder") != null) {
channel.pipeline().addBefore(
"via-encoder",
"tuff_chunk_handler",
new ChunkHandler(viaBlocks, y0, player)
);
} else {
channel.pipeline().addFirst("tuff_chunk_handler",
new ChunkHandler(viaBlocks, y0, player));
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
@@ -0,0 +1,57 @@
package tf.tuff.tuffactions;
import java.util.logging.Level;
import tf.tuff.TuffX;
public abstract class TuffActionBase {
private boolean enabled = false;
private boolean debugMode = false;
private final String name;
private final String configPath;
protected final TuffActions actsPlugin;
protected final TuffX plugin;
public TuffActionBase(TuffActions actsPlugin, String name, String configPath, boolean defaultEnabled) {
this.actsPlugin = actsPlugin;
this.plugin = actsPlugin.plugin;
this.name = name;
this.configPath = configPath;
plugin.getConfig().addDefault(configPath+".enabled", defaultEnabled);
plugin.getConfig().addDefault(configPath+".debug", false);
}
public boolean isEnabled() {
return enabled;
}
public void onConfigLoad() {
boolean wasEnabled = this.enabled;
this.enabled = plugin.getConfig().getBoolean(this.configPath+".enabled");
if (enabled) {
this.enable(wasEnabled);
} else if (wasEnabled) {
this.disable();
}
this.debugMode = plugin.getConfig().getBoolean(this.configPath+".debug");
}
protected void enable(boolean wasEnabled) {
actsPlugin.info(name+" enabled.");
}
protected void disable() {
actsPlugin.info(name+" is now disabled.");
}
protected boolean isDebug() {
return debugMode;
}
protected void debug(String msg) {
if (debugMode) actsPlugin.log(Level.INFO, msg);
}
protected void debug(String msg, Exception e) {
if (debugMode) actsPlugin.log(Level.INFO, msg, e);
}
}
@@ -0,0 +1,159 @@
package tf.tuff.tuffactions;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityToggleSwimEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerPluginMessage;
import tf.tuff.TuffX;
import tf.tuff.tuffactions.creative.CreativeMenu;
import tf.tuff.tuffactions.restrictions.Restrictions;
import tf.tuff.tuffactions.swimming.Swimming;
public class TuffActions {
public static final String CHANNEL = "eagler:tuffactions";
private Swimming swimmingManager;
private CreativeMenu creativeManager;
private Restrictions restrictions;
public final TuffX plugin;
public static final Set<UUID> tuffPlayers = ConcurrentHashMap.newKeySet();
public TuffActions(TuffX plugin){
this.plugin = plugin;
}
private void loadConfig() {
info("TuffActions has been enabled");
info("Enabling features...");
swimmingManager.onConfigLoad();
creativeManager.onConfigLoad();
restrictions.onConfigLoad();
}
public void onTuffXReload() {
loadConfig();
info("Misc features reloaded.");
}
public void onTuffXEnable() {
this.swimmingManager = new Swimming(this);
this.creativeManager = new CreativeMenu(this);
this.restrictions = new Restrictions(this);
plugin.getConfig().options().copyDefaults(true);
loadConfig();
info("Finished enabling features.");
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "eagler:tuffactions");
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, "eagler:tuffactions", plugin);
}
public boolean onTuffXCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equalsIgnoreCase("restrictions")) return this.restrictions.onTuffXCommand(sender, command, label, args);
return true;
}
public void handlePacket(Player player, byte[] message) {
if (player == null || message == null || message.length < 13) return;
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(message))) {
in.readInt();
in.readInt();
in.readInt();
int actionLength = in.readUnsignedByte();
if (actionLength == 0 || in.available() < actionLength) return;
byte[] actionBytes = new byte[actionLength];
in.readFully(actionBytes);
String action = new String(actionBytes, StandardCharsets.UTF_8);
tuffPlayers.add(player.getUniqueId());
if ("swimming_state".equals(action)) {
swimmingManager.handleSwimState(player, in.readBoolean());
} else if ("elytra_state".equals(action)) {
swimmingManager.handleElytraState(player, in.readBoolean());
} else if ("swim_ready".equals(action)){
swimmingManager.handleSwimReady(player);
} else if ("creative_ready".equals(action)){
creativeManager.handleCreativeReady(player);
} else if ("give_creative_item".equals(action)){
if (!creativeManager.isEnabled()) return;
if (player.getGameMode() != GameMode.CREATIVE) return;
int itemLength = in.readUnsignedByte();
if (in.available() < itemLength + Integer.BYTES) return;
byte[] itemBytes = new byte[itemLength];
in.readFully(itemBytes);
String item = new String(itemBytes, StandardCharsets.UTF_8);
int amount = in.readInt();
creativeManager.handlePlaceholderTaken(player, item, amount);
} else if ("pick_viablock".equals(action)){
if (!creativeManager.isEnabled()) return;
if (player.getGameMode() != GameMode.CREATIVE) return;
int blockLength = in.readUnsignedByte();
if (in.available() < blockLength + 1) return;
byte[] blockBytes = new byte[blockLength];
in.readFully(blockBytes);
String blockName = new String(blockBytes, StandardCharsets.UTF_8);
int hotbarSlot = in.readUnsignedByte();
creativeManager.handlePickViablock(player, blockName, hotbarSlot);
} else if ("restrictions_ready".equals(action)) {
restrictions.handleRestrictionsReady(player);
}
} catch (IOException e) {
log(Level.WARNING, "Failed to read a plugin message from " + player.getName(), e);
}
}
public void sendPluginMessage(Player player, byte[] payload) {
sendPluginMessage(player, CHANNEL, payload);
}
public void sendPluginMessage(Player player, String channel, byte[] payload) {
if (player == null || payload == null || !player.isOnline() || PacketEvents.getAPI() == null || !PacketEvents.getAPI().isInitialized()) return;
WrapperPlayServerPluginMessage packet = new WrapperPlayServerPluginMessage(channel, payload);
PacketEvents.getAPI().getPlayerManager().sendPacket(player, packet);
}
public void handlePlayerQuit(PlayerQuitEvent event) {
swimmingManager.handlePlayerQuit(event);
tuffPlayers.remove(event.getPlayer().getUniqueId());
}
public void handleToggleSwim(EntityToggleSwimEvent event) {
swimmingManager.handleToggleSwim(event);
}
public void handlePlayerInventoryClick(InventoryClickEvent event) {
creativeManager.onPlayerInventoryClick(event);
}
public void log(Level level, String msg, Throwable e) {
plugin.getLogger().log(level, "[TuffActions] "+msg, e);
}
public void log(Level level, String msg) {
plugin.getLogger().log(level, "[TuffActions] "+msg);
}
public void info(String msg) {
log(Level.INFO, msg);
}
}
@@ -0,0 +1,115 @@
package tf.tuff.tuffactions.creative;
import tf.tuff.tuffactions.TuffActionBase;
import tf.tuff.tuffactions.TuffActions;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import tf.tuff.util.SchedulerCompat;
public class CreativeMenu extends TuffActionBase {
private final Set<String> itemMapping = ConcurrentHashMap.newKeySet();
private final Map<UUID, ItemStack> playerHoldingPlaceholder = new ConcurrentHashMap<>();
private final TabUtil tabUtil;
public CreativeMenu(TuffActions actsPlugin) {
super(actsPlugin, "Creative Items", "creative-items", true);
this.tabUtil = new TabUtil(actsPlugin);
}
@Override
protected void enable(boolean wasEnabled) {
if (!wasEnabled) {
if (itemMapping.isEmpty()) initializeMappings();
}
super.enable(wasEnabled);
}
@Override
protected void disable() {
itemMapping.clear();
super.disable();
}
public void initializeMappings() {
for (Material m : Material.values()){
if (m.isItem()){
itemMapping.add(m.name());
}
}
}
/*** CUSTOM SERVER-BOUND PACKETS ***/
public void handleCreativeReady(Player player) {
if (!isEnabled()) return;
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("creative_items");
out.writeInt(itemMapping.size());
for (String item : itemMapping) {
out.writeUTF(item);
String category = this.tabUtil.getCreativeCategory(item);
out.writeUTF(category != null ? category : "");
}
actsPlugin.sendPluginMessage(player, bout.toByteArray());
} catch (IOException e) {
debug("Failed to send creative items to " + player.getName(), e);
}
}
public void handlePlaceholderTaken(Player player, String realItemName, int amount) {
Material realMaterial = Material.getMaterial(realItemName.toUpperCase(Locale.ROOT));
if (realMaterial != null) {
ItemStack realItemStack = new ItemStack(realMaterial, amount);
playerHoldingPlaceholder.put(player.getUniqueId(), realItemStack);
}
}
public void handlePickViablock(Player player, String blockName, int hotbarSlot) {
Material material = Material.getMaterial(blockName.toUpperCase(Locale.ROOT));
if (material == null || !material.isItem()) {
return;
}
if (hotbarSlot < 0 || hotbarSlot > 8) {
hotbarSlot = 0;
}
player.getInventory().setItem(hotbarSlot, new ItemStack(material, 1));
}
/*** EVENT HANDLERS ***/
public void onPlayerInventoryClick(InventoryClickEvent event) {
if (!isEnabled()) return;
Player player = (Player) event.getWhoClicked();
UUID playerUUID = player.getUniqueId();
if (playerHoldingPlaceholder.containsKey(playerUUID)) {
InventoryAction action = event.getAction();
if (action == InventoryAction.PLACE_ALL || action == InventoryAction.PLACE_ONE || action == InventoryAction.SWAP_WITH_CURSOR) {
SchedulerCompat.runEntityLater(player, plugin, () -> {
ItemStack realItemStack = playerHoldingPlaceholder.get(playerUUID);
if (realItemStack != null && event.getClickedInventory() != null) {
event.getClickedInventory().setItem(event.getSlot(), realItemStack);
}
playerHoldingPlaceholder.remove(playerUUID);
}, 1L);
}
}
}
}
@@ -0,0 +1,58 @@
package tf.tuff.tuffactions.creative;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
import javax.annotation.Nullable;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import tf.tuff.tuffactions.TuffActions;
public class TabUtil {
private final TuffActions plugin;
private final File mappingFile;
private Map<String, String> creativeTabMap;
public TabUtil(TuffActions plugin) {
this.plugin = plugin;
this.mappingFile = new File(plugin.plugin.getDataFolder(), "tab-mapping.json");
setupMappingFile();
loadMapping();
}
private void setupMappingFile() {
if (!mappingFile.exists()) {
plugin.info("Creative tab mapping not found, creating from resources...");
plugin.plugin.saveResource("tab-mapping.json", false);
}
}
private void loadMapping() {
try {
ObjectMapper mapper = new ObjectMapper();
TypeReference<Map<String, String>> typeRef = new TypeReference<Map<String, String>>() {};
this.creativeTabMap = mapper.readValue(mappingFile, typeRef);
plugin.info("Successfully loaded " + creativeTabMap.size() + " creative tab mappings.");
} catch (IOException e) {
plugin.log(Level.SEVERE, "Failed to load creative tab mapping from file!", e);
this.creativeTabMap = Collections.emptyMap();
}
}
@Nullable
public String getCreativeCategory(String material) {
if (material == null || creativeTabMap.isEmpty()) {
return null;
}
return creativeTabMap.get(material);
}
}
@@ -0,0 +1,102 @@
package tf.tuff.tuffactions.restrictions;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import tf.tuff.tuffactions.TuffActionBase;
import tf.tuff.tuffactions.TuffActions;
public class Restrictions extends TuffActionBase {
private RestrictionsCommand commandHandler;
private Set<String> disallowed = ConcurrentHashMap.newKeySet();
private static final List<String> example = List.of("clientbrand");
public Restrictions(TuffActions actsPlugin) {
super(actsPlugin, "Restrictions", "restrictions", true);
this.commandHandler = new RestrictionsCommand(this, plugin);
plugin.getConfig().addDefault("restrictions.disallow", example);
}
public void loadConfig() {
disallowed.clear();
actsPlugin.info("Loading Restrictions config...");
List<?> config = plugin.getConfig().getList("restrictions.disallow");
for (Object val : config) {
if (val instanceof String) disallowed.add((String)val);
}
}
@Override
protected void enable(boolean wasEnabled) {
loadConfig();
super.enable(wasEnabled);
}
@Override
protected void disable() {
disallowed.clear();
super.disable();
}
/*** CUSTOM SERVER-BOUND PACKETS ***/
public void handleRestrictionsReady(Player player) {
if (!isEnabled()) return;
debug("Sending restrictions to %s".formatted(player.getName()));
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("client_features_all");
out.writeInt(disallowed.size());
for (String key : disallowed) {
out.writeUTF(key);
out.writeBoolean(false);
}
actsPlugin.sendPluginMessage(player, bout.toByteArray());
} catch (IOException e) {
debug("Failed to send Restrictions to " + player.getName(), e);
}
}
/*** CUSTOM CLIENT-BOUND PACKETS ***/
public void sendSingleUpdateToAll(String key) {
for (UUID uuid : TuffActions.tuffPlayers) {
Player player = Bukkit.getPlayer(uuid);
handleSingleUpdate(player, key);
}
}
private void handleSingleUpdate(Player player, String key) {
if (!isEnabled()) return;
debug("Sending restriction update for '%s' to %s".formatted(key, player.getName()));
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("client_feature");
out.writeUTF(key);
out.writeBoolean(!disallowed.contains(key));
actsPlugin.sendPluginMessage(player, bout.toByteArray());
} catch (IOException e) {
actsPlugin.log(Level.WARNING, "Failed to send Restriction "+key+" to " + player.getName(), e);
}
}
/*** COMMANDS ***/
public boolean onTuffXCommand(CommandSender sender, Command command, String label, String[] args) {
if (!isEnabled()) {
sender.sendMessage("Restrictions are currently disabled.");
return true;
}
return commandHandler.onCommand(sender, command, label, args);
}
}
@@ -0,0 +1,74 @@
package tf.tuff.tuffactions.restrictions;
import java.util.List;
import java.util.logging.Level;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import tf.tuff.TuffX;
public class RestrictionsCommand {
private final TuffX tuffx;
private final Restrictions restrictions;
RestrictionsCommand(Restrictions restrictions, TuffX tuffx) {
this.restrictions = restrictions;
this.tuffx = tuffx;
}
private boolean error(CommandSender sender, String msg) {
sender.sendMessage("\u00A7c%s".formatted(msg));
return true;
}
private boolean noPermission(CommandSender sender) {
return error(sender, "You do not have permission to use this command.");
}
private boolean invalidUsage(CommandSender sender, String msg) {
return error(sender, "Invalid usage. %s Use: /restrictions <allow|disallow> <module>".formatted(msg));
}
private boolean unknownError(CommandSender sender, Exception e) {
tuffx.getLogger().log(Level.SEVERE, "An unknown error occurred while executing a restrictions command", e);
return error(sender, "An unknown error has occurred. Check server logs.");
}
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) return invalidUsage(sender, "'allow' or 'disallow' required.");
if (args[0].equalsIgnoreCase("disallow")) {
if (!sender.hasPermission("tuffx.restrictions.command.disallow")) return noPermission(sender);
if (args.length < 2) return invalidUsage(sender, "Module name required.");
try {
@SuppressWarnings("unchecked")
List<String> config = (List<String>)tuffx.getConfig().getList("restrictions.disallow");
config.add(args[1]);
tuffx.getConfig().set("restrictions.disallow", config);
tuffx.saveConfig();
restrictions.loadConfig();
restrictions.sendSingleUpdateToAll(args[1]);
} catch(Exception e) {
return unknownError(sender, e);
}
sender.sendMessage("\u00A7aModule '%s' added to disallow list.".formatted(args[1]));
return true;
} else if (args[0].equalsIgnoreCase("allow")) {
if (!sender.hasPermission("tuffx.restrictions.command.allow")) return noPermission(sender);
if (args.length < 2) return invalidUsage(sender, "Module name required.");
try {
@SuppressWarnings("unchecked")
List<String> config = (List<String>)tuffx.getConfig().getList("restrictions.disallow");
if (!config.contains(args[1])) return error(sender, "Module '%s' not in disallow list. No change.".formatted(args[1]));
config.remove(args[1]);
tuffx.getConfig().set("restrictions.disallow", config);
tuffx.saveConfig();
restrictions.loadConfig();
restrictions.sendSingleUpdateToAll(args[1]);
} catch(Exception e) {
return unknownError(sender, e);
}
sender.sendMessage("\u00A7aModule '%s' removed from disallow list.".formatted(args[1]));
return true;
}
return invalidUsage(sender, "Unknown option.");
}
}
@@ -0,0 +1,166 @@
package tf.tuff.tuffactions.swimming;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityToggleSwimEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
import io.github.retrooper.packetevents.util.folia.TaskWrapper;
import tf.tuff.tuffactions.TuffActionBase;
import tf.tuff.tuffactions.TuffActions;
import tf.tuff.util.SchedulerCompat;
public class Swimming extends TuffActionBase {
private final Set<UUID> swimmingPlayers = ConcurrentHashMap.newKeySet();
private final Map<UUID, TaskWrapper> swimStateTasks = new ConcurrentHashMap<>();
public Swimming(TuffActions plugin) {
super(plugin, "Swimming", "swimming", true);
}
@Override
protected void disable() {
for (TaskWrapper task : swimStateTasks.values()) {
task.cancel();
}
swimStateTasks.clear();
swimmingPlayers.clear();
super.disable();
}
/*** CUSTOM, SERVER-BOUND PACKETS ***/
public void handleSwimReady(Player player) {
if (!isEnabled()) return;
SchedulerCompat.runEntityLater(player, plugin, () -> {
for (UUID swimmingPlayerId : swimmingPlayers) {
Player swimmingPlayer = Bukkit.getPlayer(swimmingPlayerId);
if (swimmingPlayer != null && swimmingPlayer.isOnline() && player.canSee(swimmingPlayer)) {
sendSwimState(player, swimmingPlayer, true);
}
}
}, 20L);
}
public void handleSwimState(Player player, boolean isSwimming) {
if (!isEnabled()) return;
if (isSwimming) {
swimmingPlayers.add(player.getUniqueId());
startSwimMaintenance(player);
} else {
swimmingPlayers.remove(player.getUniqueId());
stopSwimMaintenance(player.getUniqueId());
}
SchedulerCompat.runEntity(player, plugin, () -> applySwimmingState(player, isSwimming));
broadcastSwimState(player, isSwimming);
}
public void handleElytraState(Player player, boolean isGliding) {
if (!isEnabled()) return;
SchedulerCompat.runEntity(player, plugin, () -> {
ItemStack chest = player.getInventory().getChestplate();
if (chest != null && chest.getType() == Material.ELYTRA) player.setGliding(isGliding);
});
}
/*** EVENT HANDLERS ***/
public void handleToggleSwim(EntityToggleSwimEvent event) {
if (!isEnabled()) return;
if (!(event.getEntity() instanceof Player)) return;
Player player = (Player) event.getEntity();
if (!event.isSwimming() && swimmingPlayers.contains(player.getUniqueId())) {
event.setCancelled(true);
SchedulerCompat.runEntity(player, plugin, () -> {
if (swimmingPlayers.contains(player.getUniqueId()) && player.isOnline()) {
applySwimmingState(player, true);
}
});
}
}
public void handlePlayerQuit(PlayerQuitEvent event) {
if (!isEnabled()) return;
Player player = event.getPlayer();
stopSwimMaintenance(player.getUniqueId());
if (swimmingPlayers.remove(player.getUniqueId())) {
broadcastSwimState(player, false);
}
}
/*** CUSTOM CLIENT-BOUND PACKETS ***/
private void broadcastSwimState(Player subject, boolean isSwimming) {
for (UUID otherUUID : TuffActions.tuffPlayers) {
if (!otherUUID.equals(subject.getUniqueId())) {
Player recipient = Bukkit.getPlayer(otherUUID);
if (recipient != null && recipient.isOnline()) {
SchedulerCompat.runEntity(recipient, plugin, () -> {
if (recipient.isOnline() && recipient.canSee(subject)) {
sendSwimState(recipient, subject, isSwimming);
}
});
}
}
}
}
private void sendSwimState(Player recipient, Player subject, boolean isSwimming) {
if (recipient == null || !recipient.isOnline()) return;
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("update_other_swim");
out.writeLong(subject.getUniqueId().getMostSignificantBits());
out.writeLong(subject.getUniqueId().getLeastSignificantBits());
out.writeBoolean(isSwimming);
actsPlugin.sendPluginMessage(recipient, bout.toByteArray());
} catch (IOException e) {
debug("Failed to send swim state to " + recipient.getName(), e);
}
}
private void maintainSwimmingState(Player player) {
if (!player.isOnline()) {
stopSwimMaintenance(player.getUniqueId());
swimmingPlayers.remove(player.getUniqueId());
return;
}
if (!player.isInWater()) {
stopSwimMaintenance(player.getUniqueId());
swimmingPlayers.remove(player.getUniqueId());
applySwimmingState(player, false);
broadcastSwimState(player, false);
return;
}
applySwimmingState(player, true);
}
private void applySwimmingState(Player player, boolean swimming) {
if (player == null || !player.isOnline()) return;
if (swimming && !player.isInWater()) return;
if (player.isSwimming() != swimming) {
player.setSwimming(swimming);
}
}
private void startSwimMaintenance(Player player) {
swimStateTasks.computeIfAbsent(player.getUniqueId(),
ignored -> SchedulerCompat.runEntityTimer(player, plugin, () -> maintainSwimmingState(player), 1L, 1L));
}
private void stopSwimMaintenance(UUID playerId) {
TaskWrapper task = swimStateTasks.remove(playerId);
if (task != null) {
task.cancel();
}
}
}
@@ -0,0 +1,82 @@
package tf.tuff.util;
import java.util.concurrent.TimeUnit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import io.github.retrooper.packetevents.util.folia.FoliaScheduler;
import io.github.retrooper.packetevents.util.folia.TaskWrapper;
public final class SchedulerCompat {
private SchedulerCompat() {
}
public static boolean isFolia() {
return FoliaScheduler.isFolia();
}
public static void runGlobal(Plugin plugin, Runnable task) {
FoliaScheduler.getGlobalRegionScheduler().execute(plugin, task);
}
public static TaskWrapper runGlobalLater(Plugin plugin, Runnable task, long delayTicks) {
return FoliaScheduler.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> task.run(), delayTicks);
}
public static TaskWrapper runGlobalTimer(Plugin plugin, Runnable task, long delayTicks, long periodTicks) {
return FoliaScheduler.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delayTicks, periodTicks);
}
public static void runAsync(Plugin plugin, Runnable task) {
FoliaScheduler.getAsyncScheduler().runNow(plugin, scheduledTask -> task.run());
}
public static TaskWrapper runAsyncLater(Plugin plugin, Runnable task, long delayTicks) {
return FoliaScheduler.getAsyncScheduler().runDelayed(plugin, scheduledTask -> task.run(), delayTicks * 50L, TimeUnit.MILLISECONDS);
}
public static void runRegion(Plugin plugin, World world, int chunkX, int chunkZ, Runnable task) {
FoliaScheduler.getRegionScheduler().execute(plugin, world, chunkX, chunkZ, task);
}
public static TaskWrapper runRegionLater(Plugin plugin, World world, int chunkX, int chunkZ, Runnable task, long delayTicks) {
return FoliaScheduler.getRegionScheduler().runDelayed(plugin, world, chunkX, chunkZ, scheduledTask -> task.run(), delayTicks);
}
public static void runRegion(Plugin plugin, Location location, Runnable task) {
FoliaScheduler.getRegionScheduler().execute(plugin, location, task);
}
public static TaskWrapper runRegionLater(Plugin plugin, Location location, Runnable task, long delayTicks) {
return FoliaScheduler.getRegionScheduler().runDelayed(plugin, location, scheduledTask -> task.run(), delayTicks);
}
public static void runEntity(Entity entity, Plugin plugin, Runnable task) {
FoliaScheduler.getEntityScheduler().execute(entity, plugin, task, () -> {
}, 0L);
}
public static TaskWrapper runEntityLater(Entity entity, Plugin plugin, Runnable task, long delayTicks) {
return FoliaScheduler.getEntityScheduler().runDelayed(entity, plugin, scheduledTask -> task.run(), () -> {
}, delayTicks);
}
public static TaskWrapper runEntityTimer(Entity entity, Plugin plugin, Runnable task, long delayTicks, long periodTicks) {
return FoliaScheduler.getEntityScheduler().runAtFixedRate(entity, plugin, scheduledTask -> task.run(), () -> {
}, delayTicks, periodTicks);
}
public static void sendPluginMessage(Plugin plugin, Player player, String channel, byte[] payload) {
if (player == null || channel == null || payload == null || !player.isOnline()) return;
runEntity(player, plugin, () -> {
if (player.isOnline()) {
player.sendPluginMessage(plugin, channel, payload);
}
});
}
}
@@ -0,0 +1,678 @@
package tf.tuff.viablocks;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Door;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.block.*;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import tf.tuff.netty.ChunkInjector;
import tf.tuff.util.SchedulerCompat;
import tf.tuff.viablocks.version.VersionAdapter;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
public class CustomBlockListener {
public final ViaBlocksPlugin plugin;
private final VersionAdapter versionAdapter;
private final PaletteManager paletteManager;
private final EnumSet<Material> modernMaterials;
private ChunkInjector chunkInjector;
private static final long X_MASK = (1L << 26) - 1L;
private static final long Z_MASK = (1L << 26) - 1L;
private static final long Y_MASK = (1L << 12) - 1L;
private static final int Z_SHIFT = 12;
private static final int X_SHIFT = 12 + 26;
private final Map<UUID, Map<Integer, List<Long>>> pendingUpdates = new HashMap<>();
private final Set<UUID> pendingFlush = new HashSet<>();
private static final double UPDATE_RADIUS_SQUARED = 6400;
private static final @Nonnull byte[] EMPTY_PACKET = new byte[0];
private final Cache<BlockData, Integer> blockDataIdCache;
private final Cache<String, byte[]> chunkPacketCache;
private final Cache<Long, Integer> recentModernChanges;
public CustomBlockListener(ViaBlocksPlugin plugin, VersionAdapter versionAdapter, PaletteManager paletteManager) {
this.plugin = plugin;
this.versionAdapter = versionAdapter;
this.paletteManager = paletteManager;
this.modernMaterials = versionAdapter.getModernMaterials();
this.blockDataIdCache = CacheBuilder.newBuilder()
.maximumSize(8000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
this.chunkPacketCache = CacheBuilder.newBuilder()
.maximumSize(4096)
.expireAfterAccess(5, TimeUnit.MINUTES)
.build();
this.recentModernChanges = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
public byte[] getCachedChunkData(String worldName, int x, int z) {
return chunkPacketCache.getIfPresent(chunkKey(worldName, x, z));
}
public void setChunkInjector(ChunkInjector injector) {
if (injector == null) return;
this.chunkInjector = injector;
}
private @Nonnull String chunkKey(String worldName, int x, int z) {
return worldName + "_" + x + "_" + z;
}
public void onViaBlocksPlayerJoin(Player player) {
if (plugin.isFirstJoin(player)) {
plugin.sendWelcomeGui(player);
plugin.markPlayerAsJoined(player);
}
sendPaletteToClient(player);
preCacheVisibleChunks(player);
if (chunkInjector != null) {
chunkInjector.inject(player);
}
sendInitialChunks(player);
}
private void preCacheVisibleChunks(Player player) {
World world = player.getWorld();
int viewDistance = Math.min(this.versionAdapter.getClientViewDistance(player), 16);
int px = player.getLocation().getChunk().getX();
int pz = player.getLocation().getChunk().getZ();
for (int x = -viewDistance; x <= viewDistance; x++) {
for (int z = -viewDistance; z <= viewDistance; z++) {
int cx = px + x;
int cz = pz + z;
if (world.isChunkLoaded(cx, cz)) {
Chunk chunk = world.getChunkAt(cx, cz);
prepareChunkCache(chunk);
}
}
}
}
private static final int CHUNKS_PER_TICK = 8;
private void sendInitialChunks(Player player) {
World world = player.getWorld();
int viewDistance = Math.min(this.versionAdapter.getClientViewDistance(player), 16);
int px = player.getLocation().getChunk().getX();
int pz = player.getLocation().getChunk().getZ();
List<int[]> chunks = new ArrayList<>();
for (int x = -viewDistance; x <= viewDistance; x++) {
for (int z = -viewDistance; z <= viewDistance; z++) {
int cx = px + x;
int cz = pz + z;
if (world.isChunkLoaded(cx, cz)) {
chunks.add(new int[]{cx, cz, x * x + z * z});
}
}
}
chunks.sort((a, b) -> Integer.compare(a[2], b[2]));
sendChunksBatched(player, world.getName(), chunks, 0);
}
private void sendChunksBatched(Player player, String worldName, List<int[]> chunks, int startIndex) {
if (!player.isOnline() || startIndex >= chunks.size()) return;
int endIndex = Math.min(startIndex + CHUNKS_PER_TICK, chunks.size());
for (int i = startIndex; i < endIndex; i++) {
int[] chunk = chunks.get(i);
World world = player.getWorld();
if (!world.getName().equals(worldName)) {
return;
}
cacheChunkWithCallback(world, chunk[0], chunk[1], data -> {
if (!player.isOnline() || !plugin.isPlayerEnabled(player)) return;
if (data != null && data.length > 0) {
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data);
}
});
}
if (endIndex < chunks.size()) {
final int nextStart = endIndex;
if (player.isOnline()) {
SchedulerCompat.runEntityLater(player, plugin.plugin, () -> sendChunksBatched(player, worldName, chunks, nextStart), 1L);
}
}
}
public void handlePlayerQuit(PlayerQuitEvent event) {
if (chunkInjector != null) {
chunkInjector.eject(event.getPlayer());
}
UUID playerId = event.getPlayer().getUniqueId();
pendingUpdates.remove(playerId);
pendingFlush.remove(playerId);
plugin.viaBlocksEnabledPlayers.remove(playerId);
plugin.setPlayerEnabled(event.getPlayer(), false);
}
public void handleChunkLoad(ChunkLoadEvent event) {
if (!plugin.isEnabled() || !hasViaBlocksPlayersInWorld(event.getWorld())) return;
prepareChunkCache(event.getChunk());
}
public void prepareChunkCache(Chunk chunk) {
if (!plugin.isEnabled() || !chunk.isLoaded() || modernMaterials.isEmpty()) return;
if (plugin.chunkExecutor == null || plugin.chunkExecutor.isShutdown()) return;
String key = chunkKey(chunk.getWorld().getName(), chunk.getX(), chunk.getZ());
if (chunkPacketCache.getIfPresent(key) != null) return;
ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, false, false);
int minHeight = chunk.getWorld().getMinHeight();
int maxHeight = chunk.getWorld().getMaxHeight();
plugin.chunkExecutor.submit(() -> {
try {
if (chunkPacketCache.getIfPresent(key) != null) return;
Map<Integer, List<Long>> foundBlocks = findModernBlocksInChunk(snapshot, minHeight, maxHeight);
if (foundBlocks.isEmpty()) {
chunkPacketCache.put(key, EMPTY_PACKET);
} else {
chunkPacketCache.put(key, buildChunkPacket(foundBlocks));
}
} catch (Exception e) {}
});
}
public byte[] getExtraDataForChunk(String worldName, int x, int z) {
return chunkPacketCache.getIfPresent(chunkKey(worldName, x, z));
}
public void cacheChunkWithCallback(World world, int x, int z, Consumer<byte[]> callback) {
if (!plugin.isEnabled()) {
deliverCallback(callback, null);
return;
}
String key = chunkKey(world.getName(), x, z);
byte[] existing = chunkPacketCache.getIfPresent(key);
if (existing != null) {
deliverCallback(callback, existing.length > 0 ? existing : null);
return;
}
if (!world.isChunkLoaded(x, z)) {
chunkPacketCache.put(key, EMPTY_PACKET);
deliverCallback(callback, null);
return;
}
Chunk chunk = world.getChunkAt(x, z);
if (!chunk.isLoaded() || modernMaterials.isEmpty()) {
chunkPacketCache.put(key, EMPTY_PACKET);
deliverCallback(callback, null);
return;
}
if (plugin.chunkExecutor == null || plugin.chunkExecutor.isShutdown()) {
chunkPacketCache.put(key, EMPTY_PACKET);
deliverCallback(callback, null);
return;
}
ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, false, false);
int minHeight = world.getMinHeight();
int maxHeight = world.getMaxHeight();
plugin.chunkExecutor.submit(() -> {
try {
byte[] cached = chunkPacketCache.getIfPresent(key);
if (cached != null) {
deliverCallback(callback, cached.length > 0 ? cached : null);
return;
}
Map<Integer, List<Long>> foundBlocks = findModernBlocksInChunk(snapshot, minHeight, maxHeight);
if (foundBlocks.isEmpty()) {
chunkPacketCache.put(key, EMPTY_PACKET);
deliverCallback(callback, null);
} else {
@SuppressWarnings("null")
@Nonnull byte[] data = buildChunkPacket(foundBlocks);
chunkPacketCache.put(key, data);
deliverCallback(callback, data);
}
} catch (Exception e) {
deliverCallback(callback, null);
}
});
}
@SuppressWarnings("unchecked")
private Map<Integer, List<Long>> findModernBlocksInChunk(ChunkSnapshot chunkSnapshot, int minHeight, int maxHeight) {
Int2ObjectMap<LongList> foundBlocks = new Int2ObjectOpenHashMap<>();
int chunkX = chunkSnapshot.getX() << 4;
int chunkZ = chunkSnapshot.getZ() << 4;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minHeight; y < maxHeight; y++) {
// Check material FIRST — getBlockType() returns an enum, no allocation
Material blockType = chunkSnapshot.getBlockType(x, y, z);
if (blockType == Material.AIR
|| blockType == Material.CAVE_AIR
|| blockType == Material.VOID_AIR
|| !this.modernMaterials.contains(blockType)) {
continue;
}
// Only allocate BlockData for confirmed modern blocks
@SuppressWarnings("null")
@Nonnull BlockData data = chunkSnapshot.getBlockData(x, y, z);
Integer cachedId = blockDataIdCache.getIfPresent(data);
int materialId;
if (cachedId != null) {
materialId = cachedId;
} else {
materialId = this.paletteManager.getOrCreateId(data.getAsString());
blockDataIdCache.put(data, materialId);
}
if (materialId != -1) {
long packedLocation = packLocation(chunkX + x, y, chunkZ + z);
LongList locs = foundBlocks.get(materialId);
if (locs == null) {
locs = new LongArrayList();
foundBlocks.put(materialId, locs);
}
locs.add(packedLocation);
}
}
}
}
return (Map<Integer, List<Long>>) (Map<?, ?>) foundBlocks;
}
public byte[] getExtraDataForMultiBlock(World world, List<Long> locations) {
Map<Integer, List<Long>> foundBlocks = new HashMap<>();
for (long packedLoc : locations) {
Integer cachedId = recentModernChanges.getIfPresent(packedLoc);
if (cachedId != null) {
List<Long> locs = foundBlocks.get(cachedId);
if (locs == null) {
locs = new ArrayList<>();
foundBlocks.put(cachedId, locs);
}
locs.add(packedLoc);
continue;
}
int x = (int) (packedLoc >> 38);
int y = (int) ((packedLoc << 52) >> 52);
int z = (int) ((packedLoc << 26) >> 38);
Block block = world.getBlockAt(x, y, z);
BlockData data = block.getBlockData();
if (isModernMaterial(data.getMaterial())) {
int id = getMaterialId(data);
if (id != -1) {
List<Long> locs2 = foundBlocks.get(id);
if (locs2 == null) {
locs2 = new ArrayList<>();
foundBlocks.put(id, locs2);
}
locs2.add(packedLoc);
}
}
}
if (foundBlocks.isEmpty()) return null;
return buildChunkPacket(foundBlocks);
}
public byte[] getExtraDataForSingleBlock(World world, int x, int y, int z) {
Block block = world.getBlockAt(x, y, z);
BlockData data = block.getBlockData();
long packed = packLocation(x, y, z);
if (!isModernMaterial(data.getMaterial())) {
Integer cachedId = recentModernChanges.getIfPresent(packed);
if (cachedId != null && cachedId > 0) {
recentModernChanges.put(packed, 0);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("ADD_SINGLE");
out.writeInt(0);
out.writeLong(packed);
return out.toByteArray();
}
return null;
}
int id = getMaterialId(data);
if (id == -1) return null;
recentModernChanges.put(packed, id);
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("ADD_SINGLE");
out.writeInt(id);
out.writeLong(packed);
return out.toByteArray();
}
public void handleBlockPlace(BlockPlaceEvent event) {
handleModernBlockChange(
event.getBlockReplacedState().getBlockData(),
event.getBlock().getBlockData(),
event.getBlock().getLocation()
);
}
public void handleBlockBreak(BlockBreakEvent event) {
Block block = event.getBlock();
BlockData data = block.getBlockData();
Location loc = block.getLocation();
handleModernBlockChange(data, Material.AIR.createBlockData(), loc);
if (data instanceof Door) {
Door door = (Door) data;
Location otherHalf = door.getHalf() == Bisected.Half.BOTTOM
? loc.clone().add(0, 1, 0)
: loc.clone().add(0, -1, 0);
long otherPacked = packLocation(otherHalf);
recentModernChanges.put(otherPacked, 0);
invalidateChunkCache(otherHalf.getChunk());
}
}
public void handleBlockExplode(BlockExplodeEvent event) {
for (Block block : event.blockList()) {
handleModernBlockChange(
block.getBlockData(),
Material.AIR.createBlockData(),
block.getLocation()
);
}
}
public void handleBlockFromTo(BlockFromToEvent event) {
Block destroyedBlock = event.getToBlock();
handleModernBlockChange(
destroyedBlock.getBlockData(),
Material.AIR.createBlockData(),
destroyedBlock.getLocation()
);
}
public void handleBlockGrow(BlockGrowEvent event) {
handleModernBlockChange(
event.getBlock().getBlockData(),
event.getNewState().getBlockData(),
event.getBlock().getLocation()
);
}
public void handleBlockFade(BlockFadeEvent event) {
handleModernBlockChange(
event.getBlock().getBlockData(),
event.getNewState().getBlockData(),
event.getBlock().getLocation()
);
}
public void handleBlockForm(BlockFormEvent event) {
handleModernBlockChange(
event.getBlock().getBlockData(),
event.getNewState().getBlockData(),
event.getBlock().getLocation()
);
}
public void handleBlockSpread(BlockSpreadEvent event) {
handleModernBlockChange(
event.getBlock().getBlockData(),
event.getNewState().getBlockData(),
event.getBlock().getLocation()
);
}
public void handleBlockPhysics(BlockPhysicsEvent event) {
Block block = event.getBlock();
if (isModernMaterial(block.getType())) {
BlockData data = block.getBlockData();
long packed = packLocation(block.getLocation());
Integer cachedId = recentModernChanges.getIfPresent(packed);
int currentId = getMaterialId(data);
if (cachedId == null || cachedId != currentId) {
recentModernChanges.put(packed, currentId);
sendBlockStateUpdateToNearbyPlayers(block.getLocation(), data);
invalidateChunkCache(block.getChunk());
}
}
}
private void handleModernBlockChange(BlockData before, BlockData after, Location location) {
if (!plugin.isEnabled() || location == null) return;
boolean afterModern = after != null && isModernMaterial(after.getMaterial());
boolean beforeModern = before != null && isModernMaterial(before.getMaterial());
long packed = packLocation(location);
if (afterModern) {
int id = getMaterialId(after);
recentModernChanges.put(packed, id);
sendBlockStateUpdateToNearbyPlayers(location, after);
} else if (beforeModern || recentModernChanges.getIfPresent(packed) != null) {
recentModernChanges.put(packed, 0);
sendClearUpdateToNearbyPlayers(location);
} else {
recentModernChanges.invalidate(packed);
}
invalidateChunkCache(location.getChunk());
}
public Integer getRecentChange(long packed) {
return recentModernChanges.getIfPresent(packed);
}
private void sendBlockStateUpdateToNearbyPlayers(Location location, BlockData data) {
if (!plugin.isEnabled() || data == null || location.getWorld() == null) return;
Integer cachedId = blockDataIdCache.getIfPresent(data);
int stateId;
if (cachedId != null) {
stateId = cachedId;
} else {
stateId = this.paletteManager.getOrCreateId(data.getAsString());
blockDataIdCache.put(data, stateId);
}
if (stateId == -1) return;
scheduleNearbyEnabledPlayers(location, player -> sendPacket(player, stateId, location));
}
private void sendClearUpdateToNearbyPlayers(Location location) {
if (!plugin.isEnabled() || plugin.viaBlocksEnabledPlayers.isEmpty() || location.getWorld() == null) return;
final int AIR_ID = 0;
scheduleNearbyEnabledPlayers(location, player -> sendPacket(player, AIR_ID, location));
}
private void invalidateChunkCache(Chunk chunk) {
if (chunk == null) return;
chunkPacketCache.invalidate(chunkKey(chunk.getWorld().getName(), chunk.getX(), chunk.getZ()));
}
private void sendPacket(Player player, int stateId, Location location) {
if (!player.isOnline()) return;
UUID playerId = player.getUniqueId();
Map<Integer, List<Long>> updateData = pendingUpdates.get(playerId);
if (updateData == null) {
updateData = new HashMap<>();
pendingUpdates.put(playerId, updateData);
}
List<Long> stateList = updateData.get(stateId);
if (stateList == null) {
stateList = new ArrayList<>();
updateData.put(stateId, stateList);
}
stateList.add(packLocation(location));
if (pendingFlush.add(playerId)) {
SchedulerCompat.runEntityLater(player, plugin.plugin, () -> flushPendingUpdates(playerId), plugin.getUpdateBatchDelayTicks());
}
}
private void flushPendingUpdates(UUID playerId) {
Map<Integer, List<Long>> updateData = pendingUpdates.remove(playerId);
pendingFlush.remove(playerId);
if (updateData == null || updateData.isEmpty()) return;
Player player = plugin.plugin.getServer().getPlayer(playerId);
if (player == null || !player.isOnline()) return;
byte[] packetData = buildChunkPacket(updateData);
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, packetData);
}
private int getMaterialId(BlockData data) {
@SuppressWarnings("null")
Integer cachedId = blockDataIdCache.getIfPresent(data);
if (cachedId != null) return cachedId;
int id = this.paletteManager.getOrCreateId(data.getAsString());
blockDataIdCache.put(data, id);
return id;
}
@SuppressWarnings("null")
private @Nonnull byte[] buildChunkPacket(Map<Integer, List<Long>> blockData) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("ADD_CHUNK");
out.writeInt(blockData.size());
for (Map.Entry<Integer, List<Long>> entry : blockData.entrySet()) {
out.writeInt(entry.getKey());
out.writeInt(entry.getValue().size());
for (Long loc : entry.getValue()) {
out.writeLong(loc);
}
}
return out.toByteArray();
}
public void sendPaletteToClient(Player player) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("INIT_PALETTE");
List<String> palette = this.paletteManager.getPalette();
out.writeInt(palette.size());
for (String state : palette) {
out.writeUTF(state);
}
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, out.toByteArray());
}
public boolean isModernMaterial(Material material) {
return this.modernMaterials.contains(material);
}
public void processChunkForSinglePlayer(Chunk chunk, Player player) {
if (!chunk.isLoaded() || !plugin.isPlayerEnabled(player)) return;
cacheChunkWithCallback(chunk.getWorld(), chunk.getX(), chunk.getZ(), data -> {
if (!player.isOnline() || !plugin.isPlayerEnabled(player)) return;
if (data != null && data.length > 0) {
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data);
}
});
}
public void clearCache() {
blockDataIdCache.invalidateAll();
pendingUpdates.clear();
pendingFlush.clear();
chunkPacketCache.invalidateAll();
recentModernChanges.invalidateAll();
}
private void deliverCallback(Consumer<byte[]> callback, byte[] data) {
if (callback != null) {
callback.accept(data);
}
}
private boolean hasViaBlocksPlayersInWorld(World world) {
for (UUID playerId : plugin.viaBlocksEnabledPlayers) {
Player player = Bukkit.getPlayer(playerId);
if (player != null && player.isOnline() && world.equals(player.getWorld())) {
return true;
}
}
return false;
}
private void scheduleNearbyEnabledPlayers(Location location, Consumer<Player> action) {
World world = location.getWorld();
if (world == null) return;
SchedulerCompat.runGlobal(plugin.plugin, () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (!plugin.isPlayerEnabled(player)) continue;
SchedulerCompat.runEntity(player, plugin.plugin, () -> {
if (!player.isOnline()) return;
if (!world.equals(player.getWorld())) return;
if (player.getLocation().distanceSquared(location) >= UPDATE_RADIUS_SQUARED) return;
action.accept(player);
});
}
});
}
public long packLocation(int x, int y, int z) {
return ((long)x & X_MASK) << X_SHIFT | ((long)z & Z_MASK) << Z_SHIFT | ((long)y & Y_MASK);
}
public long packLocation(Location loc) {
return packLocation(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
}
@@ -0,0 +1,137 @@
package tf.tuff.viablocks;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import tf.tuff.util.SchedulerCompat;
import tf.tuff.viablocks.version.VersionAdapter;
public class PaletteManager {
ViaBlocksPlugin plugin;
private final CopyOnWriteArrayList<String> palette = new CopyOnWriteArrayList<>();
private final Map<String, Integer> stateToIdMap = new ConcurrentHashMap<>();
private volatile List<String> paletteSnapshot;
public PaletteManager(VersionAdapter versionAdapter) {
plugin = ViaBlocksPlugin.instance;
generate(versionAdapter);
}
private void generate(VersionAdapter versionAdapter) {
plugin.info("Generating ViaBlocks Pre-Defined Palette...");
List<String> initialPalette = new ArrayList<>();
for (Material material : versionAdapter.getModernMaterials()) {
if (material.isBlock()) {
addEntryInternal(initialPalette, versionAdapter.getMaterialKey(material));
}
}
String[] thickness = {"tip", "frustum", "middle", "base"};
String[] direction = {"up", "down"};
String[] bools = {"true", "false"};
String[] facings = {"north", "south", "east", "west"};
String[] tilts = {"none", "unbalanced", "partial", "full"};
for (String d : direction) {
for (String t : thickness) {
for (String w : bools) {
addEntryInternal(initialPalette, "minecraft:pointed_dripstone[thickness=" + t + ",vertical_direction=" + d + ",waterlogged=" + w + "]");
}
}
}
for (String f : facings) {
for (String t : tilts) {
for (String w : bools) {
addEntryInternal(initialPalette, "minecraft:big_dripleaf[facing=" + f + ",tilt=" + t + ",waterlogged=" + w + "]");
}
}
}
for (String f : facings) {
for (String h : new String[]{"lower", "upper"}) {
addEntryInternal(initialPalette, "minecraft:small_dripleaf[facing=" + f + ",half=" + h + ",waterlogged=false]");
}
}
for (String b : bools) {
addEntryInternal(initialPalette, "minecraft:cave_vines[age=0,berries=" + b + "]");
addEntryInternal(initialPalette, "minecraft:cave_vines_plant[berries=" + b + "]");
}
palette.addAll(initialPalette);
paletteSnapshot = new ArrayList<>(palette);
plugin.info("Palette initialized with " + palette.size() + " entries.");
}
private void addEntryInternal(List<String> localPalette, String state) {
if (!stateToIdMap.containsKey(state)) {
stateToIdMap.put(state, localPalette.size());
localPalette.add(state);
}
}
public int getOrCreateId(String state) {
Integer id = stateToIdMap.get(state);
if (id != null) {
return id;
}
synchronized (this) {
id = stateToIdMap.get(state);
if (id != null) return id;
int newId = palette.size();
palette.add(state);
stateToIdMap.put(state, newId);
paletteSnapshot = null;
broadcastNewPaletteEntry(state);
return newId;
}
}
private void broadcastNewPaletteEntry(String state) {
if (plugin == null || !plugin.plugin.isEnabled() || !plugin.isEnabled()) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("NEW_PALETTE_ENTRY");
out.writeUTF(state);
byte[] data = out.toByteArray();
SchedulerCompat.runGlobal(plugin.plugin, () -> {
if (!plugin.plugin.isEnabled() || !plugin.isEnabled()) return;
for (Player player : Bukkit.getOnlinePlayers()) {
if (plugin.isPlayerEnabled(player)) {
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data);
}
}
});
}
public synchronized int getId(String state) {
return stateToIdMap.getOrDefault(state, -1);
}
public synchronized List<String> getPalette() {
List<String> snapshot = paletteSnapshot;
if (snapshot == null) {
snapshot = new ArrayList<>(palette);
paletteSnapshot = snapshot;
}
return snapshot;
}
}
@@ -0,0 +1,317 @@
package tf.tuff.viablocks;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import tf.tuff.viablocks.version.VersionAdapter;
import tf.tuff.viablocks.version.modern.ModernAdapter;
import tf.tuff.TuffX;
import tf.tuff.util.SchedulerCompat;
public final class ViaBlocksPlugin {
public static final String CLIENTBOUND_CHANNEL = "viablocks:data";
public static final String SERVERBOUND_CHANNEL = "viablocks:handshake";
public final Set<UUID> viaBlocksEnabledPlayers = ConcurrentHashMap.newKeySet();
public CustomBlockListener blockListener;
static ViaBlocksPlugin instance;
private File playerDataFile;
private FileConfiguration playerDataConfig;
private final Set<UUID> joinedPlayersCache = ConcurrentHashMap.newKeySet();
private boolean enabled;
private boolean debug;
private boolean sendWelcomeBook;
public VersionAdapter versionAdapter;
public PaletteManager paletteManager;
private long updateBatchDelayTicks = 1L;
public ExecutorService chunkExecutor;
public TuffX plugin;
public ViaBlocksPlugin(TuffX plugin){
this.plugin = plugin;
}
public void onTuffXReload() {
Set<UUID> previouslyEnabledPlayers = ConcurrentHashMap.newKeySet();
previouslyEnabledPlayers.addAll(viaBlocksEnabledPlayers);
loadSyncSettings();
if (chunkExecutor != null) {
chunkExecutor.shutdownNow();
}
this.chunkExecutor = enabled
? Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors()))
: null;
if (playerDataFile == null) {
playerDataFile = new File(plugin.getDataFolder(), "players.yml");
}
playerDataConfig = YamlConfiguration.loadConfiguration(playerDataFile);
if (blockListener != null) {
blockListener.clearCache();
}
viaBlocksEnabledPlayers.clear();
if (enabled && blockListener != null) {
for (Player player : plugin.getServer().getOnlinePlayers()) {
if (!previouslyEnabledPlayers.contains(player.getUniqueId())) continue;
setPlayerEnabled(player, true);
blockListener.onViaBlocksPlayerJoin(player);
}
}
info("ViaBlocks reloaded.");
}
public void onTuffXEnable() {
instance = this;
this.versionAdapter = new ModernAdapter();
this.paletteManager = new PaletteManager(this.versionAdapter);
plugin.saveDefaultConfig();
loadSyncSettings();
this.chunkExecutor = enabled
? Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors()))
: null;
setupPlayerData();
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, CLIENTBOUND_CHANNEL);
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, SERVERBOUND_CHANNEL, plugin);
this.blockListener = new CustomBlockListener(this, this.versionAdapter, this.paletteManager);
plugin.getCommand("viablocks").setExecutor(plugin);
if (enabled) {
info("ViaBlocks has been enabled successfully and is listening for client handshakes.");
} else {
info("ViaBlocks is disabled in config.");
}
}
public void handlePacket(Player player, byte[] message) {
if (!isEnabled() || blockListener == null) return;
if (!isPlayerEnabled(player) && isEnabled()) {
debug("Received ViaBlocks handshake from player: " + player.getName() + ". Enabling custom blocks.");
setPlayerEnabled(player, true);
blockListener.onViaBlocksPlayerJoin(player);
}
}
public PaletteManager getPaletteManager() {
return this.paletteManager;
}
public long getUpdateBatchDelayTicks() {
return this.updateBatchDelayTicks;
}
private void loadSyncSettings() {
enabled = plugin.getConfig().getBoolean("viablocks.viablocks-enabled", false);
debug = plugin.getConfig().getBoolean("viablocks.debug", false);
sendWelcomeBook = plugin.getConfig().getBoolean("viablocks.send-welcome-book", true);
String mode = plugin.getConfig().getString("viablocks.sync-mode", "normal");
if (mode == null) {
mode = "normal";
}
this.updateBatchDelayTicks = mode.equalsIgnoreCase("reduced") ? 10L : 1L;
}
public void onTuffXDisable(){
plugin.getServer().getMessenger().unregisterOutgoingPluginChannel(plugin, CLIENTBOUND_CHANNEL);
plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin, SERVERBOUND_CHANNEL);
if (chunkExecutor != null) {
chunkExecutor.shutdownNow();
chunkExecutor = null;
}
viaBlocksEnabledPlayers.clear();
info("ViaBlocks has been disabled.");
}
private void setupPlayerData() {
playerDataFile = new File(plugin.getDataFolder(), "players.yml");
if (!playerDataFile.exists()) {
try {
playerDataFile.createNewFile();
} catch (IOException e) {
severe("Could not create players.yml!");
e.printStackTrace();
}
}
playerDataConfig = YamlConfiguration.loadConfiguration(playerDataFile);
joinedPlayersCache.clear();
for (String uuidStr : playerDataConfig.getStringList("joined-players")) {
try {
joinedPlayersCache.add(UUID.fromString(uuidStr));
} catch (IllegalArgumentException ignored) {
}
}
}
public boolean hasPlayerJoinedBefore(Player player) {
return joinedPlayersCache.contains(player.getUniqueId());
}
public boolean isFirstJoin(Player player) {
return !hasPlayerJoinedBefore(player);
}
public void markPlayerAsJoined(Player player) {
UUID uuid = player.getUniqueId();
if (joinedPlayersCache.add(uuid)) {
List<String> joinedPlayers = playerDataConfig.getStringList("joined-players");
joinedPlayers.add(uuid.toString());
playerDataConfig.set("joined-players", joinedPlayers);
try {
playerDataConfig.save(playerDataFile);
} catch (IOException e) {
severe("Could not save to players.yml!");
e.printStackTrace();
}
}
}
public void sendWelcomeGui(Player player) {
if (!this.sendWelcomeBook) return;
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta meta = (BookMeta) book.getItemMeta();
if (meta == null) return;
meta.setTitle("ViaBlocks Information");
meta.setAuthor("ViaBlocks");
TextComponent welcome = new TextComponent("Welcome to ViaBlocks!");
welcome.setColor(ChatColor.DARK_AQUA);
welcome.setBold(true);
TextComponent body = new TextComponent("\n\nThis feature is in active development!\n\nIf you find any visual bugs or issues, please report them on our ");
body.setColor(ChatColor.BLACK);
TextComponent link = new TextComponent("bug tracker");
link.setColor(ChatColor.BLUE);
link.setUnderlined(true);
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/TuffNetwork/Tuff-Client-Builds/issues"));
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(new ComponentBuilder("Click to open the bug tracker!").color(ChatColor.GRAY).create())));
TextComponent disclaimer = new TextComponent("\n\n(Bamboo and kelp are noted.)");
disclaimer.setColor(ChatColor.DARK_GRAY);
disclaimer.setItalic(true);
meta.spigot().addPage(new ComponentBuilder("").append(welcome).append(body).append(link).append(new TextComponent(".")).append(disclaimer).create());
book.setItemMeta(meta);
SchedulerCompat.runEntity(player, plugin, () -> player.openBook(book));
}
public boolean isEnabled() {
return enabled;
}
public boolean isPlayerEnabled(Player player) {
if (player == null) return false;
return viaBlocksEnabledPlayers.contains(player.getUniqueId());
}
public void setPlayerEnabled(Player player, boolean enabled) {
if (enabled && isEnabled()) {
viaBlocksEnabledPlayers.add(player.getUniqueId());
} else {
viaBlocksEnabledPlayers.remove(player.getUniqueId());
}
}
public CustomBlockListener getBlockListener() {
return this.blockListener;
}
public boolean onTuffXCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("This command can only be executed by a player.");
return true;
}
Player player = (Player) sender;
if (args.length > 0) {
if (args[0].equalsIgnoreCase("get")) {
if (!player.hasPermission("tuffx.viablocks.command.get")) {
player.sendMessage("\u00A7cYou do not have permission to use this command.");
return true;
}
this.versionAdapter.giveCustomBlocks(player);
player.sendMessage("\u00A7aYou have been given a set of custom blocks.");
return true;
} else if (args[0].equalsIgnoreCase("refresh")) {
if (!player.hasPermission("tuffx.viablocks.command.refresh")) {
player.sendMessage("\u00A7cYou do not have permission to use this command.");
return true;
}
if (!isEnabled() || blockListener == null) {
player.sendMessage("\u00A7cViaBlocks is disabled.");
return true;
}
player.sendMessage("\u00A7aRefreshing modern blocks in your view distance...");
World world = player.getWorld();
int viewDistance = this.versionAdapter.getClientViewDistance(player);
int playerChunkX = player.getLocation().getChunk().getX();
int playerChunkZ = player.getLocation().getChunk().getZ();
for (int x = -viewDistance; x <= viewDistance; x++) {
for (int z = -viewDistance; z <= viewDistance; z++) {
int chunkX = playerChunkX + x;
int chunkZ = playerChunkZ + z;
if (world.isChunkLoaded(chunkX, chunkZ)) {
blockListener.processChunkForSinglePlayer(world.getChunkAt(chunkX, chunkZ), player);
}
}
}
player.sendMessage("\u00A7aRefresh complete!");
return true;
}
}
player.sendMessage("\u00A7cInvalid usage. Use: /viablocks <get|refresh>");
return true;
}
public boolean isDebug() {
return debug;
}
public void debug(String message) {
if (isDebug()) info(message);
}
public void log(Level level, String msg, Throwable e) {
plugin.getLogger().log(level, "[ViaBlocks] "+msg, e);
}
public void log(Level level, String msg) {
plugin.getLogger().log(level, "[ViaBlocks] "+msg);
}
public void info(String msg) {
log(Level.INFO, msg);
}
public void severe(String msg) {
log(Level.SEVERE, msg);
}
}
@@ -0,0 +1,14 @@
package tf.tuff.viablocks.version;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import java.util.EnumSet;
public interface VersionAdapter {
String getBlockDataString(Block block);
String getMaterialKey(Material material);
int getClientViewDistance(Player player);
void giveCustomBlocks(Player player);
EnumSet<Material> getModernMaterials();
}
@@ -0,0 +1,669 @@
package tf.tuff.viablocks.version.modern;
import tf.tuff.viablocks.version.VersionAdapter;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.lang.reflect.Method;
import java.util.EnumSet;
public class ModernAdapter implements VersionAdapter {
private static final String[] MODERN_MATERIAL_NAMES = {
"TUBE_CORAL",
"BRAIN_CORAL",
"BUBBLE_CORAL",
"FIRE_CORAL",
"HORN_CORAL",
"TUBE_CORAL_BLOCK",
"BRAIN_CORAL_BLOCK",
"BUBBLE_CORAL_BLOCK",
"FIRE_CORAL_BLOCK",
"HORN_CORAL_BLOCK",
"TUBE_CORAL_FAN",
"BRAIN_CORAL_FAN",
"BUBBLE_CORAL_FAN",
"FIRE_CORAL_FAN",
"HORN_CORAL_FAN",
"TUBE_CORAL_WALL_FAN",
"BRAIN_CORAL_WALL_FAN",
"BUBBLE_CORAL_WALL_FAN",
"FIRE_CORAL_WALL_FAN",
"HORN_CORAL_WALL_FAN",
"DEAD_TUBE_CORAL",
"DEAD_BRAIN_CORAL",
"DEAD_BUBBLE_CORAL",
"DEAD_FIRE_CORAL",
"DEAD_HORN_CORAL",
"DEAD_TUBE_CORAL_BLOCK",
"DEAD_BRAIN_CORAL_BLOCK",
"DEAD_BUBBLE_CORAL_BLOCK",
"DEAD_FIRE_CORAL_BLOCK",
"DEAD_HORN_CORAL_BLOCK",
"DEAD_TUBE_CORAL_FAN",
"DEAD_BRAIN_CORAL_FAN",
"DEAD_BUBBLE_CORAL_FAN",
"DEAD_FIRE_CORAL_FAN",
"DEAD_HORN_CORAL_FAN",
"DEAD_TUBE_CORAL_WALL_FAN",
"DEAD_BRAIN_CORAL_WALL_FAN",
"DEAD_BUBBLE_CORAL_WALL_FAN",
"DEAD_FIRE_CORAL_WALL_FAN",
"DEAD_HORN_CORAL_WALL_FAN",
"BUBBLE_COLUMN",
"SEA_PICKLE",
"KELP",
"KELP_PLANT",
"DRIED_KELP_BLOCK",
"SEAGRASS",
"TALL_SEAGRASS",
"STRIPPED_OAK_LOG",
"STRIPPED_SPRUCE_LOG",
"STRIPPED_BIRCH_LOG",
"STRIPPED_JUNGLE_LOG",
"STRIPPED_ACACIA_LOG",
"STRIPPED_DARK_OAK_LOG",
"STRIPPED_OAK_WOOD",
"STRIPPED_SPRUCE_WOOD",
"STRIPPED_BIRCH_WOOD",
"STRIPPED_JUNGLE_WOOD",
"STRIPPED_ACACIA_WOOD",
"STRIPPED_DARK_OAK_WOOD",
"OAK_WOOD",
"SPRUCE_WOOD",
"BIRCH_WOOD",
"JUNGLE_WOOD",
"ACACIA_WOOD",
"DARK_OAK_WOOD",
"BLUE_ICE",
"CONDUIT",
"PRISMARINE_STAIRS",
"PRISMARINE_BRICK_STAIRS",
"DARK_PRISMARINE_STAIRS",
"PRISMARINE_SLAB",
"PRISMARINE_BRICK_SLAB",
"DARK_PRISMARINE_SLAB",
"TURTLE_EGG",
"SMOOTH_STONE",
"SMOOTH_SANDSTONE",
"SMOOTH_QUARTZ",
"SMOOTH_RED_SANDSTONE",
"CUT_SANDSTONE",
"CUT_RED_SANDSTONE",
"SMOOTH_STONE_SLAB",
"CUT_SANDSTONE_SLAB",
"CUT_RED_SANDSTONE_SLAB",
"BARREL",
"BLAST_FURNACE",
"SMOKER",
"CARTOGRAPHY_TABLE",
"FLETCHING_TABLE",
"GRINDSTONE",
"LECTERN",
"SMITHING_TABLE",
"STONECUTTER",
"BELL",
"LANTERN",
"SCAFFOLDING",
"LOOM",
"COMPOSTER",
"STONE_STAIRS",
"GRANITE_STAIRS",
"POLISHED_GRANITE_STAIRS",
"DIORITE_STAIRS",
"POLISHED_DIORITE_STAIRS",
"ANDESITE_STAIRS",
"POLISHED_ANDESITE_STAIRS",
"STONE_SLAB",
"GRANITE_SLAB",
"POLISHED_GRANITE_SLAB",
"DIORITE_SLAB",
"POLISHED_DIORITE_SLAB",
"ANDESITE_SLAB",
"POLISHED_ANDESITE_SLAB",
"MOSSY_STONE_BRICK_STAIRS",
"MOSSY_STONE_BRICK_SLAB",
"MOSSY_COBBLESTONE_STAIRS",
"MOSSY_COBBLESTONE_SLAB",
"END_STONE_BRICK_STAIRS",
"END_STONE_BRICK_SLAB",
"RED_NETHER_BRICK_STAIRS",
"RED_NETHER_BRICK_SLAB",
"SMOOTH_SANDSTONE_STAIRS",
"SMOOTH_RED_SANDSTONE_STAIRS",
"SMOOTH_QUARTZ_STAIRS",
"GRANITE_WALL",
"DIORITE_WALL",
"ANDESITE_WALL",
"SANDSTONE_WALL",
"RED_SANDSTONE_WALL",
"BRICK_WALL",
"STONE_BRICK_WALL",
"MOSSY_STONE_BRICK_WALL",
"NETHER_BRICK_WALL",
"RED_NETHER_BRICK_WALL",
"CRACKED_NETHER_BRICKS",
"CHISELED_NETHER_BRICKS",
"END_STONE_BRICK_WALL",
"PRISMARINE_WALL",
"CORNFLOWER",
"LILY_OF_THE_VALLEY",
"WITHER_ROSE",
"SWEET_BERRY_BUSH",
"BAMBOO",
"BAMBOO_SAPLING",
"JIGSAW",
"BEE_NEST",
"BEEHIVE",
"HONEY_BLOCK",
"HONEYCOMB_BLOCK",
"CRIMSON_STEM",
"WARPED_STEM",
"STRIPPED_CRIMSON_STEM",
"STRIPPED_WARPED_STEM",
"CRIMSON_HYPHAE",
"WARPED_HYPHAE",
"STRIPPED_CRIMSON_HYPHAE",
"STRIPPED_WARPED_HYPHAE",
"CRIMSON_NYLIUM",
"WARPED_NYLIUM",
"CRIMSON_PLANKS",
"WARPED_PLANKS",
"CRIMSON_STAIRS",
"WARPED_STAIRS",
"CRIMSON_SLAB",
"WARPED_SLAB",
"CRIMSON_FENCE",
"WARPED_FENCE",
"CRIMSON_DOOR",
"WARPED_DOOR",
"CRIMSON_TRAPDOOR",
"WARPED_TRAPDOOR",
"CRIMSON_FENCE_GATE",
"WARPED_FENCE_GATE",
"CRIMSON_BUTTON",
"WARPED_BUTTON",
"CRIMSON_PRESSURE_PLATE",
"WARPED_PRESSURE_PLATE",
"CRIMSON_SIGN",
"WARPED_SIGN",
"CRIMSON_WALL_SIGN",
"WARPED_WALL_SIGN",
"NETHER_GOLD_ORE",
"ANCIENT_DEBRIS",
"CRYING_OBSIDIAN",
"RESPAWN_ANCHOR",
"NETHERITE_BLOCK",
"SOUL_SOIL",
"BASALT",
"POLISHED_BASALT",
"SMOOTH_BASALT",
"SOUL_TORCH",
"SOUL_WALL_TORCH",
"SOUL_LANTERN",
"SOUL_FIRE",
"CAMPFIRE",
"SOUL_CAMPFIRE",
"SHROOMLIGHT",
"TARGET",
"BLACKSTONE",
"GILDED_BLACKSTONE",
"POLISHED_BLACKSTONE",
"POLISHED_BLACKSTONE_BRICKS",
"CHISELED_POLISHED_BLACKSTONE",
"CRACKED_POLISHED_BLACKSTONE_BRICKS",
"BLACKSTONE_STAIRS",
"POLISHED_BLACKSTONE_STAIRS",
"POLISHED_BLACKSTONE_BRICK_STAIRS",
"BLACKSTONE_SLAB",
"POLISHED_BLACKSTONE_SLAB",
"POLISHED_BLACKSTONE_BRICK_SLAB",
"BLACKSTONE_WALL",
"POLISHED_BLACKSTONE_WALL",
"POLISHED_BLACKSTONE_BRICK_WALL",
"CRIMSON_ROOTS",
"WARPED_ROOTS",
"NETHER_SPROUTS",
"CRIMSON_FUNGUS",
"WARPED_FUNGUS",
"WEEPING_VINES",
"WEEPING_VINES_PLANT",
"TWISTING_VINES",
"TWISTING_VINES_PLANT",
"CHAIN",
"IRON_CHAIN",
"LODESTONE",
"DEEPSLATE",
"INFESTED_DEEPSLATE",
"COBBLED_DEEPSLATE",
"POLISHED_DEEPSLATE",
"DEEPSLATE_BRICKS",
"CRACKED_DEEPSLATE_BRICKS",
"DEEPSLATE_TILES",
"CRACKED_DEEPSLATE_TILES",
"CHISELED_DEEPSLATE",
"COBBLED_DEEPSLATE_STAIRS",
"POLISHED_DEEPSLATE_STAIRS",
"DEEPSLATE_BRICK_STAIRS",
"DEEPSLATE_TILE_STAIRS",
"COBBLED_DEEPSLATE_SLAB",
"POLISHED_DEEPSLATE_SLAB",
"DEEPSLATE_BRICK_SLAB",
"DEEPSLATE_TILE_SLAB",
"COBBLED_DEEPSLATE_WALL",
"POLISHED_DEEPSLATE_WALL",
"DEEPSLATE_BRICK_WALL",
"DEEPSLATE_TILE_WALL",
"DEEPSLATE_COAL_ORE",
"DEEPSLATE_IRON_ORE",
"DEEPSLATE_GOLD_ORE",
"DEEPSLATE_DIAMOND_ORE",
"DEEPSLATE_LAPIS_ORE",
"DEEPSLATE_REDSTONE_ORE",
"DEEPSLATE_EMERALD_ORE",
"DEEPSLATE_COPPER_ORE",
"COPPER_ORE",
"COPPER_BLOCK",
"CUT_COPPER",
"COPPER_CHAIN",
"EXPOSED_COPPER",
"EXPOSED_CUT_COPPER",
"EXPOSED_COPPER_CHAIN",
"WEATHERED_COPPER",
"WEATHERED_CUT_COPPER",
"WEATHERED_COPPER_CHAIN",
"OXIDIZED_COPPER",
"OXIDIZED_CUT_COPPER",
"OXIDIZED_COPPER_CHAIN",
"WAXED_COPPER_BLOCK",
"WAXED_CUT_COPPER",
"WAXED_COPPER_CHAIN",
"WAXED_EXPOSED_COPPER",
"WAXED_EXPOSED_CUT_COPPER",
"WAXED_EXPOSED_COPPER_CHAIN",
"WAXED_WEATHERED_COPPER",
"WAXED_WEATHERED_CUT_COPPER",
"WAXED_WEATHERED_COPPER_CHAIN",
"WAXED_OXIDIZED_COPPER",
"WAXED_OXIDIZED_CUT_COPPER",
"WAXED_OXIDIZED_COPPER_CHAIN",
"CUT_COPPER_STAIRS",
"EXPOSED_CUT_COPPER_STAIRS",
"WEATHERED_CUT_COPPER_STAIRS",
"OXIDIZED_CUT_COPPER_STAIRS",
"CUT_COPPER_SLAB",
"EXPOSED_CUT_COPPER_SLAB",
"WEATHERED_CUT_COPPER_SLAB",
"OXIDIZED_CUT_COPPER_SLAB",
"WAXED_CUT_COPPER_STAIRS",
"WAXED_EXPOSED_CUT_COPPER_STAIRS",
"WAXED_WEATHERED_CUT_COPPER_STAIRS",
"WAXED_OXIDIZED_CUT_COPPER_STAIRS",
"WAXED_CUT_COPPER_SLAB",
"WAXED_EXPOSED_CUT_COPPER_SLAB",
"WAXED_WEATHERED_CUT_COPPER_SLAB",
"WAXED_OXIDIZED_CUT_COPPER_SLAB",
"RAW_COPPER_BLOCK",
"RAW_IRON_BLOCK",
"RAW_GOLD_BLOCK",
"TUFF",
"CALCITE",
"AMETHYST_BLOCK",
"BUDDING_AMETHYST",
"AMETHYST_CLUSTER",
"LARGE_AMETHYST_BUD",
"MEDIUM_AMETHYST_BUD",
"SMALL_AMETHYST_BUD",
"DRIPSTONE_BLOCK",
"POINTED_DRIPSTONE",
"MOSS_BLOCK",
"MOSS_CARPET",
"HANGING_ROOTS",
"ROOTED_DIRT",
"AZALEA",
"FLOWERING_AZALEA",
"AZALEA_LEAVES",
"FLOWERING_AZALEA_LEAVES",
"SPORE_BLOSSOM",
"CAVE_VINES",
"CAVE_VINES_PLANT",
"GLOW_BERRIES",
"TINTED_GLASS",
"LIGHT",
"SCULK_SENSOR",
"CANDLE",
"WHITE_CANDLE",
"ORANGE_CANDLE",
"MAGENTA_CANDLE",
"LIGHT_BLUE_CANDLE",
"YELLOW_CANDLE",
"LIME_CANDLE",
"PINK_CANDLE",
"GRAY_CANDLE",
"LIGHT_GRAY_CANDLE",
"CYAN_CANDLE",
"PURPLE_CANDLE",
"BLUE_CANDLE",
"BROWN_CANDLE",
"GREEN_CANDLE",
"RED_CANDLE",
"BLACK_CANDLE",
"CANDLE_CAKE",
"WHITE_CANDLE_CAKE",
"ORANGE_CANDLE_CAKE",
"MAGENTA_CANDLE_CAKE",
"LIGHT_BLUE_CANDLE_CAKE",
"YELLOW_CANDLE_CAKE",
"LIME_CANDLE_CAKE",
"PINK_CANDLE_CAKE",
"GRAY_CANDLE_CAKE",
"LIGHT_GRAY_CANDLE_CAKE",
"CYAN_CANDLE_CAKE",
"PURPLE_CANDLE_CAKE",
"BLUE_CANDLE_CAKE",
"BROWN_CANDLE_CAKE",
"GREEN_CANDLE_CAKE",
"RED_CANDLE_CAKE",
"BLACK_CANDLE_CAKE",
"POWDER_SNOW",
"LIGHTNING_ROD",
"GLOW_LICHEN",
"BIG_DRIPLEAF",
"BIG_DRIPLEAF_STEM",
"SMALL_DRIPLEAF",
"MANGROVE_LOG",
"STRIPPED_MANGROVE_LOG",
"MANGROVE_WOOD",
"STRIPPED_MANGROVE_WOOD",
"MANGROVE_PLANKS",
"MANGROVE_STAIRS",
"MANGROVE_SLAB",
"MANGROVE_FENCE",
"MANGROVE_FENCE_GATE",
"MANGROVE_DOOR",
"MANGROVE_TRAPDOOR",
"MANGROVE_BUTTON",
"MANGROVE_PRESSURE_PLATE",
"MANGROVE_SIGN",
"MANGROVE_WALL_SIGN",
"MANGROVE_LEAVES",
"MANGROVE_ROOTS",
"MUDDY_MANGROVE_ROOTS",
"MANGROVE_PROPAGULE",
"MUD",
"PACKED_MUD",
"MUD_BRICKS",
"MUD_BRICK_STAIRS",
"MUD_BRICK_SLAB",
"MUD_BRICK_WALL",
"PEARLESCENT_FROGLIGHT",
"VERDANT_FROGLIGHT",
"OCHRE_FROGLIGHT",
"FROGSPAWN",
"REINFORCED_DEEPSLATE",
"SCULK",
"SCULK_CATALYST",
"SCULK_SHRIEKER",
"SCULK_VEIN",
"CHERRY_LOG",
"STRIPPED_CHERRY_LOG",
"CHERRY_WOOD",
"STRIPPED_CHERRY_WOOD",
"CHERRY_PLANKS",
"CHERRY_STAIRS",
"CHERRY_SLAB",
"CHERRY_FENCE",
"CHERRY_FENCE_GATE",
"CHERRY_DOOR",
"CHERRY_TRAPDOOR",
"CHERRY_BUTTON",
"CHERRY_PRESSURE_PLATE",
"CHERRY_SIGN",
"CHERRY_WALL_SIGN",
"CHERRY_LEAVES",
"CHERRY_SAPLING",
"PINK_PETALS",
"BAMBOO_BLOCK",
"STRIPPED_BAMBOO_BLOCK",
"BAMBOO_PLANKS",
"BAMBOO_STAIRS",
"BAMBOO_SLAB",
"BAMBOO_FENCE",
"BAMBOO_FENCE_GATE",
"BAMBOO_DOOR",
"BAMBOO_TRAPDOOR",
"BAMBOO_BUTTON",
"BAMBOO_PRESSURE_PLATE",
"BAMBOO_SIGN",
"BAMBOO_WALL_SIGN",
"BAMBOO_MOSAIC",
"BAMBOO_MOSAIC_STAIRS",
"BAMBOO_MOSAIC_SLAB",
"BAMBOO_HANGING_SIGN",
"OAK_HANGING_SIGN",
"SPRUCE_HANGING_SIGN",
"BIRCH_HANGING_SIGN",
"JUNGLE_HANGING_SIGN",
"ACACIA_HANGING_SIGN",
"DARK_OAK_HANGING_SIGN",
"MANGROVE_HANGING_SIGN",
"CHERRY_HANGING_SIGN",
"CRIMSON_HANGING_SIGN",
"WARPED_HANGING_SIGN",
"CHISELED_BOOKSHELF",
"SUSPICIOUS_SAND",
"SUSPICIOUS_GRAVEL",
"DECORATED_POT",
"PIGLIN_HEAD",
"CALIBRATED_SCULK_SENSOR",
"TORCHFLOWER",
"TORCHFLOWER_CROP",
"PITCHER_PLANT",
"PITCHER_CROP",
"SNIFFER_EGG",
"BRUSH",
"CRAFTER",
"TRIAL_SPAWNER",
"VAULT",
"TUFF_STAIRS",
"TUFF_SLAB",
"TUFF_WALL",
"POLISHED_TUFF",
"POLISHED_TUFF_STAIRS",
"POLISHED_TUFF_SLAB",
"POLISHED_TUFF_WALL",
"TUFF_BRICKS",
"TUFF_BRICK_STAIRS",
"TUFF_BRICK_SLAB",
"TUFF_BRICK_WALL",
"CHISELED_TUFF",
"CHISELED_TUFF_BRICKS",
"COPPER_GRATE",
"EXPOSED_COPPER_GRATE",
"WEATHERED_COPPER_GRATE",
"OXIDIZED_COPPER_GRATE",
"WAXED_COPPER_GRATE",
"WAXED_EXPOSED_COPPER_GRATE",
"WAXED_WEATHERED_COPPER_GRATE",
"WAXED_OXIDIZED_COPPER_GRATE",
"COPPER_BULB",
"EXPOSED_COPPER_BULB",
"WEATHERED_COPPER_BULB",
"OXIDIZED_COPPER_BULB",
"WAXED_COPPER_BULB",
"WAXED_EXPOSED_COPPER_BULB",
"WAXED_WEATHERED_COPPER_BULB",
"WAXED_OXIDIZED_COPPER_BULB",
"COPPER_TRAPDOOR",
"EXPOSED_COPPER_TRAPDOOR",
"WEATHERED_COPPER_TRAPDOOR",
"OXIDIZED_COPPER_TRAPDOOR",
"WAXED_COPPER_TRAPDOOR",
"WAXED_EXPOSED_COPPER_TRAPDOOR",
"WAXED_WEATHERED_COPPER_TRAPDOOR",
"WAXED_OXIDIZED_COPPER_TRAPDOOR",
"COPPER_DOOR",
"EXPOSED_COPPER_DOOR",
"WEATHERED_COPPER_DOOR",
"OXIDIZED_COPPER_DOOR",
"WAXED_COPPER_DOOR",
"WAXED_EXPOSED_COPPER_DOOR",
"WAXED_WEATHERED_COPPER_DOOR",
"WAXED_OXIDIZED_COPPER_DOOR",
"CHISELED_COPPER",
"EXPOSED_CHISELED_COPPER",
"WEATHERED_CHISELED_COPPER",
"OXIDIZED_CHISELED_COPPER",
"WAXED_CHISELED_COPPER",
"WAXED_EXPOSED_CHISELED_COPPER",
"WAXED_WEATHERED_CHISELED_COPPER",
"WAXED_OXIDIZED_CHISELED_COPPER",
"HEAVY_CORE",
"COPPER_CHEST",
"EXPOSED_COPPER_CHEST",
"WEATHERED_COPPER_CHEST",
"OXIDIZED_COPPER_CHEST",
"WAXED_COPPER_CHEST",
"WAXED_EXPOSED_COPPER_CHEST",
"WAXED_WEATHERED_COPPER_CHEST",
"WAXED_OXIDIZED_COPPER_CHEST",
"COPPER_LANTERN",
"EXPOSED_COPPER_LANTERN",
"WEATHERED_COPPER_LANTERN",
"OXIDIZED_COPPER_LANTERN",
"WAXED_COPPER_LANTERN",
"WAXED_EXPOSED_COPPER_LANTERN",
"WAXED_WEATHERED_COPPER_LANTERN",
"WAXED_OXIDIZED_COPPER_LANTERN",
"OPEN_EYEBLOSSOM",
"CLOSED_EYEBLOSSOM",
"POLISHED_BLACKSTONE_BUTTON",
"POLISHED_BLACKSTONE_PRESSURE_PLATE",
"DARK_OAK_BUTTON",
"DARK_OAK_PRESSURE_PLATE",
"DARK_OAK_TRAPDOOR",
"JUNGLE_BUTTON",
"JUNGLE_PRESSURE_PLATE",
"JUNGLE_TRAPDOOR",
"ACACIA_BUTTON",
"ACACIA_PRESSURE_PLATE",
"ACACIA_TRAPDOOR",
"SPRUCE_BUTTON",
"SPRUCE_PRESSURE_PLATE",
"SPRUCE_TRAPDOOR",
"BIRCH_BUTTON",
"BIRCH_PRESSURE_PLATE",
"BIRCH_TRAPDOOR",
"BUSH",
"FIREFLY_BUSH",
"SHORT_DRY_GRASS",
"TALL_DRY_GRASS",
"CACTUS_FLOWER",
"LEAF_LITTER",
"WILDFLOWERS",
"WARPED_WART_BLOCK",
"PALE_OAK_LOG",
"STRIPPED_PALE_OAK_LOG",
"PALE_OAK_WOOD",
"STRIPPED_PALE_OAK_WOOD",
"PALE_OAK_PLANKS",
"PALE_OAK_STAIRS",
"PALE_OAK_SLAB",
"PALE_OAK_FENCE",
"PALE_OAK_FENCE_GATE",
"PALE_OAK_DOOR",
"PALE_OAK_TRAPDOOR",
"PALE_OAK_BUTTON",
"PALE_OAK_PRESSURE_PLATE",
"PALE_OAK_SIGN",
"PALE_OAK_HANGING_SIGN",
"PALE_OAK_WALL_SIGN",
"PALE_OAK_LEAVES",
"PALE_OAK_SAPLING",
"PALE_MOSS_BLOCK",
"PALE_MOSS_CARPET",
"PALE_HANGING_MOSS",
"CREAKING_HEART",
"RESIN_BLOCK",
"RESIN_BRICKS",
"RESIN_BRICK_STAIRS",
"RESIN_BRICK_SLAB",
"RESIN_BRICK_WALL",
"CHISELED_RESIN_BRICKS",
"SPRUCE_SHELF",
"CRIMSON_SHELF",
"BAMBOO_SHELF",
"CHERRY_SHELF",
"PALE_OAK_SHELF",
"JUNGLE_SHELF",
"MANGROVE_SHELF",
"DARK_OAK_SHELF",
"OAK_SHELF",
"WARPED_SHELF",
"BIRCH_SHELF",
"ACACIA_SHELF",
};
private static volatile Method clientViewDistanceMethod;
private static volatile boolean clientViewDistanceChecked;
@Override
public EnumSet<Material> getModernMaterials() {
EnumSet<Material> materials = EnumSet.noneOf(Material.class);
for (String name : MODERN_MATERIAL_NAMES) {
Material material = Material.matchMaterial(name);
if (material != null) {
materials.add(material);
}
}
return materials;
}
@Override
public String getBlockDataString(Block block) {
return block.getBlockData().getAsString();
}
@Override
public String getMaterialKey(Material material) {
return material.getKey().toString();
}
@Override
public int getClientViewDistance(Player player) {
if (!clientViewDistanceChecked) {
try {
clientViewDistanceMethod = player.getClass().getMethod("getClientViewDistance");
} catch (Exception e) {
clientViewDistanceMethod = null;
}
clientViewDistanceChecked = true;
}
if (clientViewDistanceMethod != null) {
try {
Object value = clientViewDistanceMethod.invoke(player);
if (value instanceof Integer) {
return (Integer) value;
}
} catch (Exception e) {
return player.getServer().getViewDistance();
}
}
return player.getServer().getViewDistance();
}
@Override
public void giveCustomBlocks(Player player) {
addItemIfPresent(player, "DEEPSLATE");
addItemIfPresent(player, "TUFF");
}
private void addItemIfPresent(Player player, String materialName) {
Material material = Material.matchMaterial(materialName);
if (material != null) {
player.getInventory().addItem(new ItemStack(material));
}
}
}
@@ -0,0 +1,400 @@
package tf.tuff.viaentities;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import org.bukkit.entity.Player;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import tf.tuff.util.SchedulerCompat;
public class EntityDataHandler extends ChannelOutboundHandlerAdapter {
private final ViaEntitiesPlugin plugin;
private final Player player;
private final EntityMappingManager entityMappingManager;
public EntityDataHandler(ViaEntitiesPlugin plugin, Player player) {
this.plugin = plugin;
this.player = player;
this.entityMappingManager = plugin.entityMappingManager;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
String className = msg.getClass().getName();
String simpleClassName = msg.getClass().getSimpleName();
if (className.contains("Bundle") || simpleClassName.contains("Bundle")) {
try {
Iterable<?> packets = null;
for (java.lang.reflect.Method m : msg.getClass().getMethods()) {
if (m.getName().equals("subPackets") && m.getParameterCount() == 0) {
Object result = m.invoke(msg);
if (result instanceof Iterable) {
packets = (Iterable<?>) result;
break;
}
}
}
if (packets == null) {
Class<?> clazz = msg.getClass();
outer:
while (clazz != null) {
for (java.lang.reflect.Field f : clazz.getDeclaredFields()) {
f.setAccessible(true);
Object val = f.get(msg);
if (val instanceof Iterable) {
packets = (Iterable<?>) val;
break outer;
}
}
clazz = clazz.getSuperclass();
}
}
if (packets != null) {
for (Object subPacket : packets) {
String subClass = subPacket.getClass().getSimpleName();
String subFullClass = subPacket.getClass().getName();
if (subClass.contains("Add") || subClass.contains("Spawn") ||
subFullClass.contains("AddEntity") || subFullClass.contains("SpawnEntity")) {
handleSpawnPacket(subPacket);
}
}
}
} catch (Exception e) {
}
}
boolean isSpawnPacket = className.contains("SpawnEntity") || className.contains("AddEntity") ||
simpleClassName.equals("PacketPlayOutSpawnEntity") ||
simpleClassName.equals("PacketPlayOutSpawnEntityLiving") ||
simpleClassName.equals("ClientboundAddEntityPacket") ||
simpleClassName.equals("ClientboundAddMobPacket");
if (!isSpawnPacket) {
try {
java.lang.reflect.Method getTypeMethod = msg.getClass().getMethod("getType");
java.lang.reflect.Method getIdMethod = msg.getClass().getMethod("getId");
if (getTypeMethod != null && getIdMethod != null) {
Object typeResult = getTypeMethod.invoke(msg);
if (typeResult != null && typeResult.toString().contains("entity")) {
isSpawnPacket = true;
}
}
} catch (NoSuchMethodException e) {
} catch (Exception e) {
}
}
if (isSpawnPacket) {
try {
handleSpawnPacket(msg);
} catch (Exception e) {
}
} else if (className.contains("EntityMetadata") || className.contains("SetEntityData") ||
simpleClassName.equals("PacketPlayOutEntityMetadata") ||
simpleClassName.equals("ClientboundSetEntityDataPacket")) {
try {
int entityId = getIntField(msg, "a", "id", "entityId");
if (entityId != -1) {
sendEntityMetadata(entityId, null);
}
} catch (Exception e) {
}
} else if (className.contains("Animation") ||
simpleClassName.equals("PacketPlayOutAnimation") ||
simpleClassName.equals("ClientboundAnimatePacket")) {
try {
int entityId = getIntField(msg, "a", "id", "entityId");
int animationType = getIntField(msg, "b", "action", "animationType");
if (entityId != -1) {
sendEntityAnimation(entityId, animationType);
}
} catch (Exception e) {
}
} else if (className.contains("EntityDestroy") || className.contains("RemoveEntities") ||
simpleClassName.equals("PacketPlayOutEntityDestroy") ||
simpleClassName.equals("ClientboundRemoveEntitiesPacket")) {
try {
handleDestroyPacket(msg);
} catch (Exception e) {
}
}
super.write(ctx, msg, promise);
}
private void handleSpawnPacket(Object msg) throws Exception {
int entityId = -1;
Object entityTypeObj = null;
double x = 0, y = 0, z = 0;
float yaw = 0, pitch = 0;
java.util.List<Double> doubles = new java.util.ArrayList<>();
java.util.List<Byte> bytes = new java.util.ArrayList<>();
for (java.lang.reflect.Field field : msg.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(msg);
String typeName = field.getType().getName();
if (value == null) continue;
if (typeName.contains("EntityType") || typeName.contains("EntityTypes")) {
entityTypeObj = value;
} else if (value instanceof Integer && entityId == -1) {
entityId = (Integer) value;
} else if (value instanceof Double) {
doubles.add((Double) value);
} else if (value instanceof Byte) {
bytes.add((Byte) value);
}
}
if (doubles.size() >= 3) {
x = doubles.get(0);
y = doubles.get(1);
z = doubles.get(2);
}
if (bytes.size() >= 2) {
pitch = bytes.get(0) * 360.0f / 256.0f;
yaw = bytes.get(1) * 360.0f / 256.0f;
}
if (entityId == -1) return;
if (entityTypeObj == null) return;
String entityTypeStr = entityTypeObj.toString();
String entityTypeName = extractEntityTypeName(entityTypeStr);
if (entityMappingManager.isModernEntity(entityTypeName)) {
sendEntitySpawn(entityId, entityTypeName, x, y, z, yaw, pitch);
}
}
private void handleDestroyPacket(Object msg) throws Exception {
for (java.lang.reflect.Field field : msg.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(msg);
if (value instanceof it.unimi.dsi.fastutil.ints.IntList) {
it.unimi.dsi.fastutil.ints.IntList idList = (it.unimi.dsi.fastutil.ints.IntList) value;
for (int i = 0; i < idList.size(); i++) {
sendEntityDestroy(idList.getInt(i));
}
return;
} else if (value instanceof int[]) {
for (int id : (int[]) value) {
sendEntityDestroy(id);
}
return;
} else if (value instanceof java.util.List) {
for (Object item : (java.util.List<?>) value) {
if (item instanceof Integer) {
sendEntityDestroy((Integer) item);
}
}
return;
}
}
}
private int getIntField(Object msg, String... fieldNames) {
for (String name : fieldNames) {
try {
java.lang.reflect.Field field = msg.getClass().getDeclaredField(name);
field.setAccessible(true);
Object value = field.get(msg);
if (value instanceof Integer) return (Integer) value;
if (value instanceof Number) return ((Number) value).intValue();
} catch (Exception ignored) {}
}
return -1;
}
private double getDoubleField(Object msg, String... fieldNames) {
for (String name : fieldNames) {
try {
java.lang.reflect.Field field = msg.getClass().getDeclaredField(name);
field.setAccessible(true);
Object value = field.get(msg);
if (value instanceof Double) return (Double) value;
if (value instanceof Number) return ((Number) value).doubleValue();
} catch (Exception ignored) {}
}
return 0.0;
}
private float getAngleField(Object msg, String... fieldNames) {
for (String name : fieldNames) {
try {
java.lang.reflect.Field field = msg.getClass().getDeclaredField(name);
field.setAccessible(true);
Object value = field.get(msg);
if (value instanceof Byte) return ((Byte) value) * 360.0f / 256.0f;
if (value instanceof Float) return (Float) value;
if (value instanceof Number) return ((Number) value).floatValue();
} catch (Exception ignored) {}
}
return 0.0f;
}
private Object getField(Object msg, String... fieldNames) {
for (String name : fieldNames) {
try {
java.lang.reflect.Field field = msg.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(msg);
} catch (Exception ignored) {}
}
return null;
}
private String extractEntityTypeName(String typeStr) {
if (typeStr == null) return null;
if (typeStr.startsWith("entity.minecraft.")) {
String name = typeStr.substring("entity.minecraft.".length());
return "minecraft:" + name;
}
if (typeStr.contains("ResourceKey[minecraft:entity_type / minecraft:")) {
int start = typeStr.indexOf("minecraft:", typeStr.indexOf("minecraft:") + 10) + 10;
int end = typeStr.indexOf("]", start);
if (end > start) {
return "minecraft:" + typeStr.substring(start, end);
}
}
if (typeStr.contains("entity_type.minecraft.")) {
int start = typeStr.indexOf("entity_type.minecraft.") + 22;
int end = typeStr.length();
for (int i = start; i < typeStr.length(); i++) {
char c = typeStr.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '_') {
end = i;
break;
}
}
return "minecraft:" + typeStr.substring(start, end);
}
if (typeStr.startsWith("minecraft:")) {
return typeStr;
}
if (typeStr.contains("minecraft:")) {
int start = typeStr.indexOf("minecraft:");
int end = typeStr.length();
for (int i = start + 10; i < typeStr.length(); i++) {
char c = typeStr.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '_' && c != ':') {
end = i;
break;
}
}
return typeStr.substring(start, end);
}
return typeStr;
}
private void sendEntitySpawn(int entityId, String entityType, double x, double y, double z, float yaw, float pitch) {
if (!plugin.isPlayerEnabled(player.getUniqueId())) return;
int paletteIndex = entityMappingManager.getEntityIndex(entityType);
if (paletteIndex == -1) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("SPAWN_ENTITY");
out.writeInt(entityId);
out.writeShort(paletteIndex);
out.writeDouble(x);
out.writeDouble(y);
out.writeDouble(z);
out.writeFloat(yaw);
out.writeFloat(pitch);
byte[] data = out.toByteArray();
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data);
}
private void sendEntityMetadata(int entityId, Object packedItems) {
if (!plugin.isPlayerEnabled(player.getUniqueId())) return;
try {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("ENTITY_METADATA");
out.writeInt(entityId);
if (packedItems instanceof java.util.List) {
java.util.List<?> items = (java.util.List<?>) packedItems;
out.writeInt(items.size());
for (Object item : items) {
java.lang.reflect.Method getIdMethod = item.getClass().getMethod("id");
int metaId = (int) getIdMethod.invoke(item);
out.writeInt(metaId);
java.lang.reflect.Method getValueMethod = item.getClass().getMethod("value");
Object value = getValueMethod.invoke(item);
if (value instanceof Boolean) {
out.writeByte(0);
out.writeBoolean((Boolean) value);
} else if (value instanceof Integer) {
out.writeByte(1);
out.writeInt((Integer) value);
} else if (value instanceof Float) {
out.writeByte(2);
out.writeFloat((Float) value);
} else if (value instanceof String) {
out.writeByte(3);
out.writeUTF((String) value);
} else if (value instanceof Byte) {
out.writeByte(4);
out.writeByte((Byte) value);
} else {
out.writeByte(-1);
}
}
} else {
out.writeInt(0);
}
byte[] data = out.toByteArray();
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data);
} catch (Exception e) {
}
}
private void sendEntityAnimation(int entityId, int animationType) {
if (!plugin.isPlayerEnabled(player.getUniqueId())) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("ENTITY_ANIMATION");
out.writeInt(entityId);
out.writeInt(animationType);
byte[] data = out.toByteArray();
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data);
}
private void sendEntityDestroy(int entityId) {
if (!plugin.isPlayerEnabled(player.getUniqueId())) return;
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("DESTROY_ENTITY");
out.writeInt(entityId);
byte[] data = out.toByteArray();
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data);
}
}
@@ -0,0 +1,63 @@
package tf.tuff.viaentities;
import io.netty.channel.ChannelHandler;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import tf.tuff.netty.BaseInjector;
import tf.tuff.util.SchedulerCompat;
public class EntityInjector extends BaseInjector {
private final ViaEntitiesPlugin plugin;
public EntityInjector(ViaEntitiesPlugin plugin) {
super("viaentities_handler");
this.plugin = plugin;
}
@Override
protected ChannelHandler createHandler(Player player) {
return new EntityDataHandler(plugin, player);
}
@Override
protected void onPostInject(Player player) {
SchedulerCompat.runEntity(player, plugin.plugin, () -> sendExistingEntities(player));
}
private void sendExistingEntities(Player player) {
int viewDistance = player.getWorld().getViewDistance() * 16;
for (Entity entity : player.getNearbyEntities(viewDistance, viewDistance, viewDistance)) {
if (entity.equals(player)) continue;
if (entity instanceof Player) continue;
double distance = entity.getLocation().distance(player.getLocation());
if (distance > viewDistance) continue;
String entityType = entity.getType().getKey().toString();
if (plugin.entityMappingManager.isModernEntity(entityType)) {
sendEntityData(player, entity.getEntityId(), entityType, entity);
}
}
}
public void sendEntityData(Player player, int entityId, String entityType, Entity entity) {
if (!plugin.isPlayerEnabled(player.getUniqueId())) return;
int paletteIndex = plugin.entityMappingManager.getEntityIndex(entityType);
if (paletteIndex == -1) return;
com.google.common.io.ByteArrayDataOutput out = com.google.common.io.ByteStreams.newDataOutput();
out.writeUTF("SPAWN_ENTITY");
out.writeInt(entityId);
out.writeShort(paletteIndex);
out.writeDouble(entity.getLocation().getX());
out.writeDouble(entity.getLocation().getY());
out.writeDouble(entity.getLocation().getZ());
out.writeFloat(entity.getLocation().getYaw());
out.writeFloat(entity.getLocation().getPitch());
SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, out.toByteArray());
}
}
@@ -0,0 +1,146 @@
package tf.tuff.viaentities;
import java.io.*;
import java.util.*;
import com.fasterxml.jackson.databind.*;
public class EntityMappingManager {
private final List<String> modernEntities = new ArrayList<>();
private final Map<String, Integer> entityToIndex = new HashMap<>();
private final Set<String> modernEntitySet = new HashSet<>();
private final Map<String, EntityInfo> entityInfoMap = new HashMap<>();
public EntityMappingManager() {
loadEntityMappingsFromJSON();
}
private void loadEntityMappingsFromJSON() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream("entity_mappings.json")) {
if (is == null) {
loadFallbackEntities();
return;
}
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(is);
JsonNode modernEntitiesNode = root.get("modern_entities");
JsonNode entitySizeNode = root.get("entity_size");
if (modernEntitiesNode != null) {
Iterator<Map.Entry<String, JsonNode>> fields = modernEntitiesNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
String entityType = entry.getKey();
JsonNode info = entry.getValue();
String addedVersion = info.has("added") ? info.get("added").asText() : "1.13";
String model = info.has("model") && !info.get("model").isNull() ? info.get("model").asText() : null;
boolean animated = info.has("animated") && info.get("animated").asBoolean();
double width = 1.0;
double height = 1.0;
if (entitySizeNode != null && entitySizeNode.has(entityType)) {
JsonNode sizeInfo = entitySizeNode.get(entityType);
width = sizeInfo.has("width") ? sizeInfo.get("width").asDouble() : 1.0;
height = sizeInfo.has("height") ? sizeInfo.get("height").asDouble() : 1.0;
}
addEntity(entityType, new EntityInfo(addedVersion, model, animated, width, height));
}
}
} catch (Exception e) {
loadFallbackEntities();
}
}
private void loadFallbackEntities() {
addEntity("minecraft:allay", new EntityInfo("1.19", "allay", true, 0.35, 0.6));
addEntity("minecraft:axolotl", new EntityInfo("1.17", "axolotl", true, 0.75, 0.42));
addEntity("minecraft:bee", new EntityInfo("1.15", "bee", true, 0.7, 0.6));
addEntity("minecraft:camel", new EntityInfo("1.20", "camel", true, 1.7, 2.375));
addEntity("minecraft:cat", new EntityInfo("1.14", "cat", true, 0.6, 0.7));
addEntity("minecraft:fox", new EntityInfo("1.14", "fox", true, 0.6, 0.7));
addEntity("minecraft:frog", new EntityInfo("1.19", "frog", true, 0.5, 0.5));
addEntity("minecraft:goat", new EntityInfo("1.17", "goat", true, 0.9, 1.3));
addEntity("minecraft:hoglin", new EntityInfo("1.16", "hoglin", true, 1.4, 1.4));
addEntity("minecraft:piglin", new EntityInfo("1.16", "piglin", true, 0.6, 1.95));
addEntity("minecraft:strider", new EntityInfo("1.16", "strider", true, 0.9, 1.7));
addEntity("minecraft:warden", new EntityInfo("1.19", "warden", true, 0.9, 2.9));
addEntity("minecraft:sniffer", new EntityInfo("1.20", "sniffer", true, 1.9, 1.75));
addEntity("minecraft:breeze", new EntityInfo("1.21", "breeze", true, 0.6, 1.77));
addEntity("minecraft:wind_charge", new EntityInfo("1.21", "wind_charge", false, 0.3125, 0.3125));
addEntity("minecraft:breeze_wind_charge", new EntityInfo("1.21", "breeze_wind_charge", false, 0.3125, 0.3125));
addEntity("minecraft:armadillo", new EntityInfo("1.20.5", "armadillo", true, 0.7, 0.65));
addEntity("minecraft:bogged", new EntityInfo("1.21", "bogged", true, 0.6, 1.99));
addEntity("minecraft:phantom", new EntityInfo("1.13", "phantom", true, 0.9, 0.5));
addEntity("minecraft:dolphin", new EntityInfo("1.13", "dolphin", true, 0.9, 0.6));
addEntity("minecraft:drowned", new EntityInfo("1.13", "drowned", true, 0.6, 1.95));
addEntity("minecraft:cod", new EntityInfo("1.13", "cod", true, 0.5, 0.3));
addEntity("minecraft:salmon", new EntityInfo("1.13", "salmon", true, 0.7, 0.4));
addEntity("minecraft:tropical_fish", new EntityInfo("1.13", "tropical_fish", true, 0.5, 0.4));
addEntity("minecraft:pufferfish", new EntityInfo("1.13", "pufferfish", true, 0.7, 0.7));
addEntity("minecraft:turtle", new EntityInfo("1.13", "turtle", true, 1.2, 0.4));
addEntity("minecraft:trident", new EntityInfo("1.13", "trident", false, 0.5, 0.5));
addEntity("minecraft:thrown_trident", new EntityInfo("1.13", "trident", false, 0.5, 0.5));
addEntity("minecraft:glow_squid", new EntityInfo("1.17", "glow_squid", true, 0.8, 0.8));
addEntity("minecraft:glow_item_frame", new EntityInfo("1.17", "glow_item_frame", false, 0.5, 0.5));
addEntity("minecraft:tadpole", new EntityInfo("1.19", "tadpole", true, 0.4, 0.3));
addEntity("minecraft:chest_boat", new EntityInfo("1.19", "chest_boat", false, 1.375, 0.5625));
addEntity("minecraft:piglin_brute", new EntityInfo("1.16", "piglin_brute", true, 0.6, 1.95));
addEntity("minecraft:zoglin", new EntityInfo("1.16", "zoglin", true, 1.4, 1.4));
}
private void addEntity(String entityType, EntityInfo info) {
if (!entityToIndex.containsKey(entityType)) {
entityToIndex.put(entityType, modernEntities.size());
modernEntities.add(entityType);
modernEntitySet.add(entityType);
entityInfoMap.put(entityType, info);
}
}
public boolean isModernEntity(String entityType) {
return modernEntitySet.contains(entityType);
}
public int getEntityIndex(String entityType) {
return entityToIndex.getOrDefault(entityType, -1);
}
public String getEntityByIndex(int index) {
if (index >= 0 && index < modernEntities.size()) {
return modernEntities.get(index);
}
return null;
}
public EntityInfo getEntityInfo(String entityType) {
return entityInfoMap.get(entityType);
}
public List<String> getAllModernEntities() {
return Collections.unmodifiableList(modernEntities);
}
public int getModernEntityCount() {
return modernEntities.size();
}
public static class EntityInfo {
public final String addedVersion;
public final String model;
public final boolean animated;
public final double width;
public final double height;
public EntityInfo(String addedVersion, String model, boolean animated, double width, double height) {
this.addedVersion = addedVersion;
this.model = model;
this.animated = animated;
this.width = width;
this.height = height;
}
}
}
@@ -0,0 +1,138 @@
package tf.tuff.viaentities;
import tf.tuff.TuffX;
import org.bukkit.entity.Player;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import tf.tuff.util.SchedulerCompat;
public final class ViaEntitiesPlugin {
public static final String CLIENTBOUND_CHANNEL = "viaentities:data";
public static final String SERVERBOUND_CHANNEL = "entities:handshake";
public final Set<UUID> viaEntitiesEnabledPlayers = new HashSet<>();
static ViaEntitiesPlugin instance;
public EntityMappingManager entityMappingManager;
private EntityInjector entityInjector;
private boolean enabled = true;
private boolean debug = false;
private int maxDistance = -1;
public TuffX plugin;
public ViaEntitiesPlugin(TuffX plugin) {
this.plugin = plugin;
plugin.getConfig().addDefault("viaentities.viaentities-enabled", true);
plugin.getConfig().addDefault("viaentities.debug", false);
plugin.getConfig().addDefault("viaentities.max-distance", -1);
}
public void onTuffXReload() {
loadConfig();
}
private void loadConfig() {
enabled = plugin.getConfig().getBoolean("viaentities.viaentities-enabled");
debug = plugin.getConfig().getBoolean("viaentities.debug");
maxDistance = plugin.getConfig().getInt("viaentities.max-distance");
}
public boolean isDebug() {
return debug;
}
public void debug(String message) {
if (isDebug()) info(message);
}
public void log(Level level, String msg) {
plugin.getLogger().log(level, "[ViaEntities] "+msg);
}
public void info(String msg) {
log(Level.INFO, msg);
}
public void onTuffXEnable() {
instance = this;
loadConfig();
this.entityMappingManager = new EntityMappingManager();
this.entityInjector = new EntityInjector(this);
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, CLIENTBOUND_CHANNEL);
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, SERVERBOUND_CHANNEL, plugin);
if (enabled) {
info("ViaEntities enabled with " + entityMappingManager.getModernEntityCount() + " modern entities");
} else {
info("ViaEntities disabled in config");
}
}
public boolean isEnabled() {
return enabled;
}
public int getMaxDistance() {
return maxDistance;
}
public void handlePacket(Player player, byte[] message) {
if (!enabled) return;
if (!isPlayerEnabled(player.getUniqueId())) {
debug("Received handshake from " + player.getName());
setPlayerEnabled(player.getUniqueId(), true);
entityInjector.inject(player);
sendPaletteToClient(player);
debug("Sent palette with " + entityMappingManager.getModernEntityCount() + " entities to " + player.getName());
}
}
private void sendPaletteToClient(Player player) {
com.google.common.io.ByteArrayDataOutput out = com.google.common.io.ByteStreams.newDataOutput();
out.writeUTF("INIT_PALETTE");
java.util.List<String> palette = entityMappingManager.getAllModernEntities();
out.writeInt(palette.size());
for (String entityType : palette) {
out.writeUTF(entityType);
}
SchedulerCompat.sendPluginMessage(plugin, player, CLIENTBOUND_CHANNEL, out.toByteArray());
}
public void handlePlayerQuit(org.bukkit.event.player.PlayerQuitEvent event) {
entityInjector.eject(event.getPlayer());
viaEntitiesEnabledPlayers.remove(event.getPlayer().getUniqueId());
}
public void onTuffXDisable() {
plugin.getServer().getMessenger().unregisterOutgoingPluginChannel(plugin, CLIENTBOUND_CHANNEL);
plugin.getServer().getMessenger().unregisterIncomingPluginChannel(plugin, SERVERBOUND_CHANNEL);
}
public boolean isPlayerEnabled(UUID playerId) {
return viaEntitiesEnabledPlayers.contains(playerId);
}
public void setPlayerEnabled(UUID playerId, boolean enabled) {
if (enabled) {
viaEntitiesEnabledPlayers.add(playerId);
} else {
viaEntitiesEnabledPlayers.remove(playerId);
}
}
public EntityInjector getEntityInjector() {
return entityInjector;
}
}
@@ -0,0 +1,29 @@
package tf.tuff.y0;
import tf.tuff.TuffX;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Player;
import tf.tuff.util.SchedulerCompat;
public class ChunkPacketListener {
public final Y0Plugin plugin;
public ChunkPacketListener(Y0Plugin plugin) {
this.plugin = plugin;
}
public void handleChunk(TuffX plugin, Player player, World world, int chunkX, int chunkZ){
if (!this.plugin.isPlayerReady(player)) return;
SchedulerCompat.runRegion(plugin, world, chunkX, chunkZ, () -> {
if (player.isOnline() && world.isChunkLoaded(chunkX, chunkZ)) {
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
this.plugin.processAndSendChunk(player, chunk);
}
});
}
}
+345
View File
@@ -0,0 +1,345 @@
package tf.tuff.y0;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viaversion.viabackwards.api.BackwardsProtocol;
import com.viaversion.viabackwards.api.data.BackwardsMappingData;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.protocol.Protocol;
import com.viaversion.viaversion.api.protocol.ProtocolPathEntry;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import tf.tuff.TuffX;
import tf.tuff.util.SchedulerCompat;
public class ViaBlockIds {
private final TuffX p;
private final Y0Plugin plugin;
private final String serverVersion;
private final File mappingsFile;
private Object2ObjectOpenHashMap<String, int[]> legacyMappings = new Object2ObjectOpenHashMap<>();
public ViaBlockIds(TuffX pl) {
p = pl;
plugin = pl.y0Plugin;
serverVersion = getServerMCVersion();
mappingsFile = new File(pl.getDataFolder(), serverVersion + "-mappings.json");
plugin.info("Server Minecraft Version: " + serverVersion);
SchedulerCompat.runGlobalLater(pl, this::initializeMappings, 1L);
}
private void initializeMappings() {
try {
if (Via.getAPI() == null) {
plugin.severe("ViaVersion API not found! Is ViaVersion installed?");
return;
}
} catch (IllegalArgumentException e) {
plugin.severe("ViaVersion API not found! Is ViaVersion installed?");
return;
}
if (!mappingsFile.exists()) {
plugin.info("Mapping file not found, generating...");
if (!p.getDataFolder().exists()) {
p.getDataFolder().mkdirs();
}
generateMappings();
} else {
plugin.info("Loading mappings from " + mappingsFile.getName());
loadMappings();
}
}
private static final int[] DEFAULT_LEGACY = {1, 0};
public int[] toLegacy(String k) {
int[] result = legacyMappings.get(k);
return result != null ? result : DEFAULT_LEGACY;
}
public int[] toLegacy(BlockData bd) {
org.bukkit.Material mat = bd.getMaterial();
if (mat == org.bukkit.Material.CHEST) {
return new int[] {54, getLegacyChestMeta(bd)};
}
if (mat == org.bukkit.Material.TRAPPED_CHEST) {
return new int[] {146, getLegacyChestMeta(bd)};
}
if (mat == org.bukkit.Material.ENDER_CHEST) {
return new int[] {130, getLegacyChestMeta(bd)};
}
String k = bd.getAsString();
if (k.startsWith("minecraft:")) {
k = k.substring(10);
}
return toLegacy(k);
}
private int getLegacyChestMeta(BlockData bd) {
if (bd instanceof org.bukkit.block.data.Directional directional) {
org.bukkit.block.BlockFace face = directional.getFacing();
switch (face) {
case NORTH:
return 2;
case SOUTH:
return 3;
case WEST:
return 4;
case EAST:
return 5;
default:
return 3;
}
}
String s = bd.getAsString();
if (s.contains("facing=north")) {
return 2;
}
if (s.contains("facing=south")) {
return 3;
}
if (s.contains("facing=west")) {
return 4;
}
if (s.contains("facing=east")) {
return 5;
}
return 3;
}
public int[] toLegacy(Block b) {
return toLegacy(b.getBlockData());
}
private String getServerMCVersion() {
String vs = Bukkit.getServer().getVersion();
int mi = vs.indexOf("MC: ");
if (mi != -1) {
int ei = vs.indexOf(')', mi);
return ei != -1 ? vs.substring(mi + 4, ei) : vs.substring(mi + 4);
}
plugin.log(Level.WARNING, "Could not detect Minecraft version. Defaulting to 1.21.");
return "1.21";
}
public static record MappingFile (String version, InputStream stream) {}
public @Nonnull MappingFile findMappingFile(String serverVers) {
String[] vp = serverVers.split("\\.");
int maj, min, pat;
try {
maj = Integer.parseInt(vp[0]);
min = Integer.parseInt(vp[1]);
pat = vp.length > 2 ? Integer.parseInt(vp[2]) : 0;
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
plugin.severe("Could not parse server version string: " + serverVers);
return new MappingFile(serverVers, p.getResource("mapping-" + serverVers + ".json"));
}
plugin.info("Searching for mappings, starting from " + serverVers + " and going down.");
for (int m = min; m >= 0; m--) {
int sp = (m == min) ? pat : 11; // if on correct minor version, start from patch, otherwise start from highest possible patch
for (int pt = sp; pt >= 0; pt--) {
String vtt = maj + "." + m + "." + pt;
String fileName = "mapping-" + vtt + ".json";
InputStream inpStream = p.getResource(fileName);
if (inpStream != null) {
if (!vtt.equals(serverVers)) {
plugin.info("Using fallback mapping file: " + fileName);
} else {
plugin.info("Found exact mapping file: " + fileName);
}
return new MappingFile(vtt, inpStream);
}
}
// Check for the major.min version without patch
String fileName = "mapping-" + maj + "." + m + ".json";
InputStream inpStream = p.getResource(fileName);
if (inpStream != null) {
plugin.info("Using fallback mapping file: " + fileName);
return new MappingFile(maj + "." + m, inpStream);
}
// Switch to 1.21.x versions after 26.0
if (maj >= 26 && m == 0) {
maj = 1;
m = 22; // will be 21 in the next iteration
}
}
plugin.severe("Could not find any suitable mapping file after checking all versions down to 1.0.0");
return new MappingFile(serverVers, null);
}
private void generateMappings() {
try {
MappingFile mapFile = findMappingFile(serverVersion);
if (mapFile.stream == null) {
plugin.severe("Failed to find mapping file for " + serverVersion + " in plugin resources!");
return;
}
ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, Object> r = mapper.readValue(mapFile.stream, Map.class);
mapFile.stream.close();
@SuppressWarnings("unchecked")
List<String> states = (List<String>) r.get("blockstates");
if (states == null) {
plugin.severe("'blockstates' key not found in JSON.");
return;
}
Object2ObjectOpenHashMap<String, int[]> newLegacyMappings = new Object2ObjectOpenHashMap<>();
plugin.info("Generating legacy mappings for " + states.size() + " block states...");
ProtocolVersion serverProto = ProtocolVersion.getClosest(mapFile.version); // start from base mappings file version
ProtocolVersion clientProto = ProtocolVersion.v1_12_2;
List<ProtocolPathEntry> protoPath = Via.getManager()
.getProtocolManager()
.getProtocolPath(clientProto, serverProto);
if (protoPath == null) {
plugin.log(Level.SEVERE, "Protocol path is null!");
return;
}
for (int i = 0; i < states.size(); i++) {
String k = states.get(i).replace("minecraft:", "");
String blockName = k.contains("[") ? k.substring(0, k.indexOf("[")) : k;
int[] legacy;
switch (blockName) {
case "chest":
legacy = new int[]{54, 0};
break;
case "ender_chest":
legacy = new int[]{130, 0};
break;
case "trapped_chest":
legacy = new int[]{146, 0};
break;
default:
legacy = convertToLegacy(protoPath, i);
break;
}
newLegacyMappings.put(k, legacy);
}
legacyMappings = newLegacyMappings;
Map<String, Object> outputMap = new Object2ObjectOpenHashMap<>();
outputMap.put("blockstates", legacyMappings);
mappingsFile.getParentFile().mkdirs();
mapper.writerWithDefaultPrettyPrinter().writeValue(mappingsFile, outputMap);
plugin.info("Successfully wrote mappings to " + mappingsFile.getName());
} catch (Exception e) {
plugin.log(Level.SEVERE, "Error generating legacy mappings.", e);
}
}
private void loadMappings() {
try {
ObjectMapper mapper = new ObjectMapper();
@SuppressWarnings("unchecked")
Map<String, Object> r = mapper.readValue(mappingsFile, Map.class);
@SuppressWarnings("unchecked")
Map<String, List<Integer>> readMap = (Map<String, List<Integer>>) r.get("blockstates");
if (readMap == null) {
plugin.severe("Invalid format in mappings file. Regenerating...");
generateMappings();
return;
}
legacyMappings = new Object2ObjectOpenHashMap<>();
for (Map.Entry<String, List<Integer>> e : readMap.entrySet()) {
String fullKey = e.getKey();
List<Integer> ll = e.getValue();
if (ll != null && ll.size() == 2) {
String blockName = fullKey.contains("[") ? fullKey.substring(0, fullKey.indexOf("[")) : fullKey;
int[] finalId;
switch (blockName) {
case "chest":
finalId = new int[]{54, 0};
break;
case "ender_chest":
finalId = new int[]{130, 0};
break;
case "trapped_chest":
finalId = new int[]{146, 0};
break;
default:
finalId = new int[]{ll.get(0), ll.get(1)};
break;
}
legacyMappings.put(fullKey, finalId);
}
}
plugin.info("Loaded " + legacyMappings.size() + " legacy mappings.");
} catch (IOException e) {
plugin.log(Level.SEVERE, "Failed to load mappings file.", e);
}
}
public int[] convertToLegacy(List<ProtocolPathEntry> protoPath, int stateId) {
for (int i = protoPath.size() - 1; i >= 0; i--) {
ProtocolPathEntry entry = protoPath.get(i);
Protocol<?, ?, ?, ?> protocol = entry.protocol();
if (protocol instanceof BackwardsProtocol) {
BackwardsMappingData mappingData = ((BackwardsProtocol<?, ?, ?, ?>) protocol).getMappingData();
if (mappingData != null && mappingData.getBlockStateMappings() != null) {
int newStateId = mappingData.getBlockStateMappings().getNewId(stateId);
if (newStateId != -1) stateId = newStateId;
}
}
}
int blockId = stateId >> 4;
int blockMetadata = stateId & 0xF;
return new int[]{blockId, blockMetadata};
}
}
+931
View File
@@ -0,0 +1,931 @@
package tf.tuff.y0;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
import javax.annotation.Nonnull;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockExplodeEvent;
import org.bukkit.event.block.BlockFromToEvent;
import org.bukkit.event.block.BlockPhysicsEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import tf.tuff.TuffX;
import tf.tuff.netty.ChunkInjector;
import tf.tuff.util.SchedulerCompat;
public class Y0Plugin {
public static final String CHANNEL = "eagler:below_y0";
public ViaBlockIds viaIds;
private ObjectOpenHashSet<String> enabledWorlds;
private boolean debug;
private int processorThreadCount;
private int cacheSize;
private int cacheExp;
private int concLevel;
private boolean kickOutdatedClients;
private final Set<UUID> readyPlayers = ConcurrentHashMap.newKeySet();
private volatile Cache<WorldChunk, ObjectArrayList<byte[]>> chunkCache;
private volatile Cache<WorldChunk, byte[]> chunkCacheCombined;
private volatile ExecutorService chunkProcessor;
private final ThreadLocal<Object2ObjectOpenHashMap<BlockData, int[]>> threadChunkData = ThreadLocal.withInitial(() -> new Object2ObjectOpenHashMap<>(256));
private final ThreadLocal<ByteArrayOutputStream> threadOut = ThreadLocal.withInitial(() -> new ByteArrayOutputStream(8256));
private final ThreadLocal<byte[]> threadData = ThreadLocal.withInitial(() -> new byte[12288]);
private TuffX plugin;
private static final int[] EMPTY_LEGACY = {1, 0};
private static final Map<BlockData, Integer> emissionCache = new ConcurrentHashMap<>();
private static Method getLightEmissionMethod;
public ChunkPacketListener chunkPacketListener;
private ChunkInjector chunkInjector;
static {
try {
getLightEmissionMethod = BlockData.class.getMethod("getLightEmission");
getLightEmissionMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
getLightEmissionMethod = null;
}
}
private static final Map<Material, Integer> legacy_light_map = Map.ofEntries(
Map.entry(Material.TORCH, 14),
Map.entry(Material.SOUL_TORCH, 10),
Map.entry(Material.LANTERN, 15),
Map.entry(Material.SOUL_LANTERN, 10),
Map.entry(Material.GLOWSTONE, 15),
Map.entry(Material.SEA_LANTERN, 15),
Map.entry(Material.REDSTONE_LAMP, 15),
Map.entry(Material.SHROOMLIGHT, 15),
Map.entry(Material.CAMPFIRE, 15),
Map.entry(Material.SOUL_CAMPFIRE, 10),
Map.entry(Material.END_ROD, 14),
Map.entry(Material.MAGMA_BLOCK, 3),
Map.entry(Material.FIRE, 15),
Map.entry(Material.SOUL_FIRE, 10),
Map.entry(Material.CANDLE, 3),
Map.entry(Material.WHITE_CANDLE, 3),
Map.entry(Material.CAKE, 0),
Map.entry(Material.CANDLE_CAKE, 3)
);
public Y0Plugin(TuffX plugin){
this.plugin = plugin;
}
private void debug(String m) {
if (debug) plugin.getLogger().info("[Y0-Debug] " + m);
}
public void log(Level level, String msg, Throwable e) {
plugin.getLogger().log(level, "[Y0] "+msg, e);
}
public void log(Level level, String msg) {
plugin.getLogger().log(level, "[Y0] "+msg);
}
public void info(String msg) {
log(Level.INFO, msg);
}
public void severe(String msg) {
log(Level.SEVERE, msg);
}
public record WorldChunk(String w, int x, int z) {}
private void loadConfig() {
debug = plugin.getConfig().getBoolean("y0.debug-mode");
ObjectArrayList<String> ewList = new ObjectArrayList<>(plugin.getConfig().getStringList("y0.enabled-worlds"));
enabledWorlds = new ObjectOpenHashSet<>(ewList.size());
if (plugin.getConfig().getBoolean("y0.y0-enabled")) enabledWorlds.addAll(ewList);
int threadSetting = plugin.getConfig().getInt("y0.chunk-processor-threads");
if (threadSetting <= 0) {
processorThreadCount = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
} else {
processorThreadCount = threadSetting;
}
cacheSize = plugin.getConfig().getInt("y0.cache-size");
cacheExp = plugin.getConfig().getInt("y0.cache-expiration");
concLevel = Runtime.getRuntime().availableProcessors();
kickOutdatedClients = plugin.getConfig().getBoolean("y0.kick-outdated-clients");
}
private void startup() {
loadConfig();
chunkCache = CacheBuilder.newBuilder()
.maximumSize(cacheSize)
.expireAfterAccess(cacheExp, TimeUnit.MINUTES)
.concurrencyLevel(concLevel)
.initialCapacity(256)
.build();
chunkCacheCombined = CacheBuilder.newBuilder()
.maximumSize(cacheSize)
.expireAfterAccess(cacheExp, TimeUnit.MINUTES)
.concurrencyLevel(concLevel)
.initialCapacity(256)
.build();
chunkProcessor = Executors.newFixedThreadPool(processorThreadCount, run -> {
Thread thread = new Thread(run, "TuffX-Chunk-" + System.nanoTime());
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY - 1);
return thread;
});
}
private void shutdown() {
if (chunkCache != null) {
chunkCache.invalidateAll();
}
if (chunkCacheCombined != null) {
chunkCacheCombined.invalidateAll();
}
if (chunkProcessor != null) {
chunkProcessor.shutdown();
try {
if (!chunkProcessor.awaitTermination(10, TimeUnit.SECONDS)) {
chunkProcessor.shutdownNow();
if (!chunkProcessor.awaitTermination(5, TimeUnit.SECONDS)) {
severe("Failed to shutdown chunk processor pool!");
}
}
} catch (InterruptedException e) {
chunkProcessor.shutdownNow();
Thread.currentThread().interrupt();
} finally {
chunkProcessor = null;
}
}
}
public void onTuffXReload() {
loadConfig();
shutdown();
startup();
emissionCache.clear();
info("Y0 reloaded.");
}
public void forceClearCache() {
if (chunkCache != null) {
chunkCache.invalidateAll();
chunkCache.cleanUp();
}
if (chunkCacheCombined != null) {
chunkCacheCombined.invalidateAll();
chunkCacheCombined.cleanUp();
}
emissionCache.clear();
}
public void onTuffXEnable() {
plugin.getConfig().addDefault("y0.y0-enabled", true);
plugin.getConfig().addDefault("y0.debug-mode", false);
plugin.getConfig().addDefault("y0.enabled-worlds", new ArrayList<>(java.util.Collections.singletonList("world")));
plugin.getConfig().addDefault("y0.chunk-processor-threads", 3);
plugin.getConfig().addDefault("y0.cache-size", 192);
plugin.getConfig().addDefault("y0.cache-expiration", 2);
plugin.getConfig().addDefault("y0.kick-outdated-clients", true);
plugin.getConfig().options().copyDefaults(true);
startup();
this.chunkPacketListener = new ChunkPacketListener(this);
plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, CHANNEL);
plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, CHANNEL, plugin);
if (viaIds == null) viaIds = new ViaBlockIds(this.plugin);
}
public record Coords(int x, int y, int z) {}
public void onTuffXDisable() {
shutdown();
readyPlayers.clear();
if (viaIds != null) viaIds = null;
}
public boolean isPlayerReady(Player player) {
if (player == null) return false;
return readyPlayers.contains(player.getUniqueId());
}
public void setChunkInjector(ChunkInjector injector) {
if (injector == null) return;
this.chunkInjector = injector;
}
public void handlePacket(Player player, byte[] data) {
try (DataInputStream i = new DataInputStream(new ByteArrayInputStream(data))) {
i.readInt();
i.readInt();
i.readInt();
int al = i.readUnsignedByte();
byte[] ab = new byte[al];
i.readFully(ab);
String subchannel = new String(ab, StandardCharsets.UTF_8);
handlePacket(player, subchannel);
} catch (IOException e) {
log(Level.WARNING, "Failed to parse plugin message from " + player.getName() + ": " + e.getMessage());
}
}
private void handlePacket(Player player, String subchannel) {
if (!enabledWorlds.contains(player.getWorld().getName()) && !subchannel.equalsIgnoreCase("ready")) {
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(false));
return;
}
switch (subchannel.toLowerCase()) {
case "ready2":
debug("Player " + player.getName() + " is READY.");
readyPlayers.add(player.getUniqueId());
if (enabledWorlds.contains(player.getWorld().getName())) {
readyPlayers.add(player.getUniqueId());
preCacheVisibleChunks(player);
if (chunkInjector != null) {
chunkInjector.inject(player);
}
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(true));
resendChunksInView(player);
} else {
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(false));
}
break;
case "use_on_block":
break;
case "ready":
if (kickOutdatedClients) {
player.kickPlayer("§cYour client is not compatible with the version of §6TuffX§c the server has installed!\n§7Please update your client.");
}
}
}
private void preCacheVisibleChunks(Player player) {
World world = player.getWorld();
int viewDistance = player.getClientViewDistance();
int playerChunkX = player.getLocation().getChunk().getX();
int playerChunkZ = player.getLocation().getChunk().getZ();
Object2ObjectOpenHashMap<BlockData, int[]> cvt = new Object2ObjectOpenHashMap<>(256);
for (int x = -viewDistance; x <= viewDistance; x++) {
for (int z = -viewDistance; z <= viewDistance; z++) {
int currentChunkX = playerChunkX + x;
int currentChunkZ = playerChunkZ + z;
if (world.isChunkLoaded(currentChunkX, currentChunkZ)) {
WorldChunk k = new WorldChunk(world.getName(), currentChunkX, currentChunkZ);
if (chunkCache.getIfPresent(k) == null) {
try {
Chunk chunk = world.getChunkAt(currentChunkX, currentChunkZ);
ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, false, false);
ObjectArrayList<byte[]> pp = new ObjectArrayList<>(4);
cvt.clear();
for (int sy = -4; sy < 0; sy++) {
byte[] sectionData = createSectionPayload(snapshot, currentChunkX, currentChunkZ, sy, cvt);
if (sectionData != null) {
pp.add(sectionData);
}
}
chunkCache.put(k, pp);
storeCombined(k, pp);
} catch (Exception e) { debug("Exception while pre-caching visible chunks for player "+player.getName()+": "+e.getMessage()); }
}
}
}
}
}
private static final int Y0_CHUNKS_PER_TICK = 8;
public void resendChunksInView(Player player) {
World world = player.getWorld();
int viewDistance = player.getClientViewDistance();
int playerChunkX = player.getLocation().getChunk().getX();
int playerChunkZ = player.getLocation().getChunk().getZ();
List<int[]> chunks = new ArrayList<>();
for (int x = -viewDistance; x <= viewDistance; x++) {
for (int z = -viewDistance; z <= viewDistance; z++) {
int currentChunkX = playerChunkX + x;
int currentChunkZ = playerChunkZ + z;
if (world.isChunkLoaded(currentChunkX, currentChunkZ)) {
chunks.add(new int[]{currentChunkX, currentChunkZ, x * x + z * z});
}
}
}
chunks.sort((a, b) -> Integer.compare(a[2], b[2]));
sendY0ChunksBatched(player, world.getName(), chunks, 0);
}
private void sendY0ChunksBatched(Player player, String worldName, List<int[]> chunks, int startIndex) {
if (!player.isOnline() || startIndex >= chunks.size()) return;
int endIndex = Math.min(startIndex + Y0_CHUNKS_PER_TICK, chunks.size());
for (int i = startIndex; i < endIndex; i++) {
int[] chunk = chunks.get(i);
WorldChunk k = new WorldChunk(worldName, chunk[0], chunk[1]);
ObjectArrayList<byte[]> cachedData = chunkCache.getIfPresent(k);
if (cachedData != null && !cachedData.isEmpty()) {
for (byte[] py : cachedData) {
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, py);
}
}
}
if (endIndex < chunks.size()) {
final int nextStart = endIndex;
if (player.isOnline()) {
SchedulerCompat.runEntityLater(player, plugin, () -> sendY0ChunksBatched(player, worldName, chunks, nextStart), 1L);
}
}
}
private byte[] y0StatusPkt(boolean s) {
try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("y0_status");
out.writeBoolean(s);
return bout.toByteArray();
} catch (IOException e) { return null; }
}
private byte[] dimensionChangePkt() {
try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("dimension_change");
return bout.toByteArray();
} catch (IOException e) { return null; }
}
public void handlePlayerChangeWorld(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, dimensionChangePkt());
boolean isEnabledWorld = enabledWorlds.contains(player.getWorld().getName());
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(isEnabledWorld));
if (isPlayerReady(player) && isEnabledWorld) {
resendChunksInView(player);
}
}
public void handlePlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, dimensionChangePkt());
boolean isEnabledWorld = enabledWorlds.contains(player.getWorld().getName());
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(isEnabledWorld));
}
public void processAndSendChunk(final Player player, final Chunk c) {
if (c == null || player == null || !player.isOnline()) return;
if (!c.getWorld().equals(player.getWorld())) return;
if (enabledWorlds != null && !enabledWorlds.contains(c.getWorld().getName())) return;
final WorldChunk k = new WorldChunk(c.getWorld().getName(), c.getX(), c.getZ());
ObjectArrayList<byte[]> cachedData = chunkCache.getIfPresent(k);
if (cachedData != null) {
if (player.isOnline() && c.getWorld().equals(player.getWorld())) {
for (byte[] py : cachedData) {
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, py);
}
}
return;
}
final ChunkSnapshot snapshot = c.getChunkSnapshot(false, false, false);
processSnapshotAsync(player, snapshot, c.getX(), c.getZ());
}
private void processSnapshotAsync(final Player player, final ChunkSnapshot snapshot, final int chunkX, final int chunkZ) {
ExecutorService executor = chunkProcessor;
if (executor == null || executor.isShutdown()) return;
final WorldChunk k = new WorldChunk(snapshot.getWorldName(), chunkX, chunkZ);
executor.submit(() -> {
final ObjectArrayList<byte[]> pp = new ObjectArrayList<>(4);
final Object2ObjectOpenHashMap<BlockData, int[]> cvt = threadChunkData.get();
cvt.clear();
for (int sy = -4; sy < 0; sy++) {
if (!player.isOnline()) {
return;
}
try {
byte[] py = createSectionPayload(snapshot, chunkX, chunkZ, sy, cvt);
if (py != null) {
pp.add(py);
}
} catch (IOException e) {
severe("Payload creation failed: " + e.getMessage());
}
}
chunkCache.put(k, pp);
storeCombined(k, pp);
if (!pp.isEmpty()) {
for (byte[] py : pp) {
SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, py);
}
}
});
}
private void invalidateChunkCache(World w, int x, int z) {
WorldChunk k = new WorldChunk(w.getName(), x >> 4, z >> 4);
chunkCache.invalidate(k);
chunkCacheCombined.invalidate(k);
}
public byte[] getY0DataForChunk(Player player, int chunkX, int chunkZ) {
if (!isPlayerReady(player)) return null;
World world = player.getWorld();
if (enabledWorlds == null || !enabledWorlds.contains(world.getName())) return null;
WorldChunk k = new WorldChunk(world.getName(), chunkX, chunkZ);
byte[] combined = chunkCacheCombined.getIfPresent(k);
if (combined != null) return combined.length > 0 ? combined : null;
ObjectArrayList<byte[]> cachedData = chunkCache.getIfPresent(k);
if (cachedData != null) {
byte[] cb = buildCombined(cachedData);
if (cb != null) chunkCacheCombined.put(k, cb);
return cb;
}
return null;
}
private byte[] buildCombined(ObjectArrayList<byte[]> sections) {
if (sections.isEmpty()) return null;
int totalLen = 0;
for (int i = 0, l = sections.size(); i < l; i++) {
totalLen += sections.get(i).length;
}
byte[] result = new byte[totalLen];
int offset = 0;
for (int i = 0, l = sections.size(); i < l; i++) {
byte[] s = sections.get(i);
System.arraycopy(s, 0, result, offset, s.length);
offset += s.length;
}
return result;
}
private void storeCombined(@Nonnull WorldChunk k, ObjectArrayList<byte[]> sections) {
byte[] cb = buildCombined(sections);
if (cb != null) {
chunkCacheCombined.put(k, cb);
}
}
public void preCacheY0Data(Player player, int chunkX, int chunkZ) {
if (!isPlayerReady(player)) return;
World world = player.getWorld();
if (enabledWorlds == null || !enabledWorlds.contains(world.getName())) return;
WorldChunk k = new WorldChunk(world.getName(), chunkX, chunkZ);
if (chunkCache.getIfPresent(k) != null) return;
if (!world.isChunkLoaded(chunkX, chunkZ)) return;
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, false, false);
ExecutorService executor = chunkProcessor;
if (executor == null || executor.isShutdown()) return;
executor.submit(() -> {
try {
if (chunkCache.getIfPresent(k) != null) return;
Object2ObjectOpenHashMap<BlockData, int[]> cvt = threadChunkData.get();
cvt.clear();
ObjectArrayList<byte[]> pp = new ObjectArrayList<>(4);
for (int sy = -4; sy < 0; sy++) {
byte[] sectionData = createSectionPayload(snapshot, chunkX, chunkZ, sy, cvt);
if (sectionData != null) {
pp.add(sectionData);
}
}
chunkCache.put(k, pp);
storeCombined(k, pp);
} catch (Exception e) { debug("Exception while pre-caching chunk %s, %s for %s: %s".formatted(chunkX, chunkZ, player.getName(), e.getMessage())); }
});
}
public void cacheChunkWithCallback(Player player, int chunkX, int chunkZ, Consumer<byte[]> callback) {
if (!isPlayerReady(player)) {
callback.accept(null);
return;
}
World world = player.getWorld();
if (enabledWorlds == null || !enabledWorlds.contains(world.getName())) {
callback.accept(null);
return;
}
WorldChunk k = new WorldChunk(world.getName(), chunkX, chunkZ);
byte[] combined = chunkCacheCombined.getIfPresent(k);
if (combined != null) {
callback.accept(combined.length > 0 ? combined : null);
return;
}
ObjectArrayList<byte[]> existing = chunkCache.getIfPresent(k);
if (existing != null) {
byte[] cb = buildCombined(existing);
if (cb != null) chunkCacheCombined.put(k, cb);
callback.accept(cb);
return;
}
if (!world.isChunkLoaded(chunkX, chunkZ)) {
callback.accept(null);
return;
}
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, false, false);
ExecutorService executor = chunkProcessor;
if (executor == null || executor.isShutdown()) {
callback.accept(null);
return;
}
executor.submit(() -> {
try {
byte[] cached = chunkCacheCombined.getIfPresent(k);
if (cached != null) {
callback.accept(cached.length > 0 ? cached : null);
return;
}
Object2ObjectOpenHashMap<BlockData, int[]> cvt = threadChunkData.get();
cvt.clear();
ObjectArrayList<byte[]> pp = new ObjectArrayList<>(4);
for (int sy = -4; sy < 0; sy++) {
byte[] sectionData = createSectionPayload(snapshot, chunkX, chunkZ, sy, cvt);
if (sectionData != null) {
pp.add(sectionData);
}
}
chunkCache.put(k, pp);
byte[] cb = buildCombined(pp);
if (cb != null) chunkCacheCombined.put(k, cb);
callback.accept(cb);
} catch (Exception e) {
callback.accept(null);
}
});
}
public void handlePlayerQuit(PlayerQuitEvent event) {
if (chunkInjector != null) {
chunkInjector.eject(event.getPlayer());
}
readyPlayers.remove(event.getPlayer().getUniqueId());
}
public void handleChunkLoad(ChunkLoadEvent event) {
if (enabledWorlds == null || !enabledWorlds.contains(event.getWorld().getName())) return;
if (!hasReadyPlayersInWorld(event.getWorld())) return;
Chunk chunk = event.getChunk();
int chunkX = chunk.getX();
int chunkZ = chunk.getZ();
WorldChunk k = new WorldChunk(event.getWorld().getName(), chunkX, chunkZ);
if (chunkCache.getIfPresent(k) != null) return;
ChunkSnapshot snapshot = chunk.getChunkSnapshot(false, false, false);
ExecutorService executor = chunkProcessor;
if (executor == null || executor.isShutdown()) return;
executor.submit(() -> {
try {
if (chunkCache.getIfPresent(k) != null) return;
Object2ObjectOpenHashMap<BlockData, int[]> cvt = threadChunkData.get();
cvt.clear();
ObjectArrayList<byte[]> pp = new ObjectArrayList<>(4);
for (int sy = -4; sy < 0; sy++) {
byte[] sectionData = createSectionPayload(snapshot, chunkX, chunkZ, sy, cvt);
if (sectionData != null) {
pp.add(sectionData);
}
}
chunkCache.put(k, pp);
storeCombined(k, pp);
} catch (Exception e) { debug("Exception while handling chunk load: "+e.getMessage()); }
});
}
private byte[] createSectionPayload(ChunkSnapshot s, int x, int z, int sy, Object2ObjectOpenHashMap<BlockData, int[]> c) throws IOException {
// Ensure thread-local buffer is exactly 12,288 bytes to prevent overflow
byte[] bd = threadData.get();
Arrays.fill(bd, (byte) 0);
int idx = 0;
boolean hasContent = false;
int by = sy << 4;
// Optimized Loop Order: Matches standard Minecraft internal memory layouts
for (int xx = 0; xx < 16; xx++) {
for (int zz = 0; zz < 16; zz++) {
for (int y = 0; y < 16; y++) {
int wy = by + y;
BlockData blkData = s.getBlockData(xx, wy, zz);
int[] ld = c.get(blkData); // Fast map lookup
if (ld == null) { // Avoid getOrDefault overhead
ld = (viaIds != null) ? viaIds.toLegacy(blkData) : EMPTY_LEGACY;
c.put(blkData, ld);
}
// Bitwise packing
short lb = (short) ((ld[1] << 12) | (ld[0] & 0xFFF));
byte pl = (byte) ((s.getBlockSkyLight(xx, wy, zz) << 4) | s.getBlockEmittedLight(xx, wy, zz));
// Write sequence
int linear = ((y << 8) | (zz << 4) | xx) * 3;
bd[linear] = (byte) (lb >> 8);
bd[linear + 1] = (byte) lb;
bd[linear + 2] = pl;
if (linear + 3 > idx) idx = linear + 3;
if (lb != 0 || pl != 0) {
hasContent = true;
}
}
}
}
if (!hasContent) return null;
ByteArrayOutputStream bout = threadOut.get();
bout.reset();
// DataOutputStream wrapper safely writes schema
try (DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("chunk_data");
out.writeInt(x);
out.writeInt(z);
out.writeInt(sy);
out.write(bd, 0, idx);
}
return bout.toByteArray();
}
public void handleBlockBreak(BlockBreakEvent event) {
if (event.getBlock().getY() < 0) {
handleBlockChange(event.getBlock().getLocation(), event.getBlock().getBlockData(), Material.AIR.createBlockData());
invalidateChunkCache(event.getBlock().getWorld(), event.getBlock().getX(), event.getBlock().getZ());
}
}
public void handleBlockPlace(BlockPlaceEvent event) {
if (event.getBlock().getY() < 0) {
handleBlockChange(event.getBlock().getLocation(), event.getBlockReplacedState().getBlockData(), event.getBlock().getBlockData());
invalidateChunkCache(event.getBlock().getWorld(), event.getBlock().getX(), event.getBlock().getZ());
}
}
public void handleBlockPhysics(BlockPhysicsEvent event) {
final Block block = event.getBlock();
if (block.getY() < 0) {
final Location loc = block.getLocation();
final World world = loc.getWorld();
SchedulerCompat.runRegionLater(plugin, loc, () -> {
BlockData ud = world.getBlockData(loc);
sendSingleBlockUpdate(loc, ud);
invalidateChunkCache(world, loc.getBlockX(), loc.getBlockZ());
}, 1L);
}
}
public void handleBlockExplode(BlockExplodeEvent event) {
final List<Block> btu = new ArrayList<>(event.blockList());
for (Block block : btu) {
if (block.getY() >= 0) continue;
final Location loc = block.getLocation();
SchedulerCompat.runRegionLater(plugin, loc, () -> {
sendSingleBlockUpdate(loc, Material.AIR.createBlockData());
invalidateChunkCache(loc.getWorld(), loc.getBlockX(), loc.getBlockZ());
}, 1L);
}
}
public void handleBlockFromTo(BlockFromToEvent event) {
final Block block = event.getToBlock();
if (block.getY() < 0) {
SchedulerCompat.runRegionLater(plugin, block.getLocation(), () -> {
sendSingleBlockUpdate(block.getLocation(), block.getBlockData());
invalidateChunkCache(block.getWorld(), block.getX(), block.getZ());
}, 1L);
}
}
private void sendSingleBlockUpdate(Location loc, BlockData data) {
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(64);
DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("block_update");
out.writeInt(loc.getBlockX());
out.writeInt(loc.getBlockY());
out.writeInt(loc.getBlockZ());
int[] ld = viaIds.toLegacy(data);
out.writeShort((short) ((ld[1] << 12) | (ld[0] & 0xFFF)));
byte[] py = bout.toByteArray();
sendToNearbyPlayers(loc, py);
} catch (IOException e) {
severe("Failed to create single block update payload: " + e.getMessage());
}
}
public static int getEmission(BlockData data) {
if (getLightEmissionMethod != null) {
try {
return (int) getLightEmissionMethod.invoke(data);
} catch (Exception e) {}
}
return legacy_light_map.getOrDefault(data.getMaterial(), 0);
}
private void handleBlockChange(Location loc, BlockData od, BlockData nd) {
sendSingleBlockUpdate(loc, nd);
boolean oe = getEmission(od) > 0;
boolean ne = getEmission(nd) > 0;
boolean oo = od.getMaterial().isOccluding();
boolean no = nd.getMaterial().isOccluding();
if (oe != ne || oo != no) {
sendLightUpdate(loc);
}
}
private void sendLightUpdate(Location loc) {
ObjectOpenHashSet<Coords> stu = new ObjectOpenHashSet<>();
World w = loc.getWorld();
int bx = loc.getBlockX();
int by = loc.getBlockY();
int bz = loc.getBlockZ();
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -1; dz <= 1; dz++) {
int ny = by + dy;
if (ny < -64 || ny >= 0) continue;
stu.add(new Coords(
(bx + dx) >> 4,
ny >> 4,
(bz + dz) >> 4
));
}
}
}
for (Coords sc : stu) {
if (!w.isChunkLoaded(sc.x, sc.z)) continue;
ChunkSnapshot s = w.getChunkAt(sc.x, sc.z).getChunkSnapshot(true, false, false);
if (chunkProcessor != null && !chunkProcessor.isShutdown()) {
chunkProcessor.submit(() -> {
try {
byte[] py = createLightPayload(s, sc);
sendToNearbyPlayers(loc, py);
} catch (IOException e) {
severe("Failed to create lighting payload: " + e.getMessage());
}
});
}
}
}
private void sendToNearbyPlayers(Location loc, byte[] payload) {
if (payload == null || loc.getWorld() == null || readyPlayers.isEmpty()) return;
final World world = loc.getWorld();
SchedulerCompat.runGlobal(plugin, () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (!player.isOnline() || !isPlayerReady(player)) continue;
if (!world.equals(player.getWorld())) continue;
if (player.getLocation().distanceSquared(loc) >= 4096) continue;
SchedulerCompat.runEntity(player, plugin, () -> {
if (!player.isOnline() || !isPlayerReady(player)) return;
if (!world.equals(player.getWorld())) return;
if (player.getLocation().distanceSquared(loc) >= 4096) return;
player.sendPluginMessage(plugin, CHANNEL, payload);
});
}
});
}
private byte[] createLightPayload(ChunkSnapshot s, Coords sc) throws IOException {
try (ByteArrayOutputStream bout = new ByteArrayOutputStream(4120);
DataOutputStream out = new DataOutputStream(bout)) {
out.writeUTF("lighting_update");
out.writeInt(sc.x);
out.writeInt(sc.z);
out.writeInt(sc.y);
byte[] ld = new byte[4096];
int by = sc.y * 16;
int i = 0;
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int wy = by + y;
int bl = s.getBlockEmittedLight(x, wy, z);
int sl = s.getBlockSkyLight(x, wy, z);
ld[i++] = (byte) ((sl << 4) | bl);
}
}
}
out.write(ld);
return bout.toByteArray();
}
}
private boolean hasReadyPlayersInWorld(World world) {
for (UUID playerId : readyPlayers) {
Player player = plugin.getServer().getPlayer(playerId);
if (player != null && player.isOnline() && world.equals(player.getWorld())) {
return true;
}
}
return false;
}
}