Initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user