From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Fri, 14 Dec 2018 21:53:58 -0800 Subject: [PATCH] Tuinity Server Changes Brand changes MC-Dev fixes Util patch Tuinity Server Config Multi-Threaded Server Ticking Vanilla This patch is the vanilla server changes Currently a placeholder patch. Multi-Threaded ticking CraftBukkit These are the changes to CB Currently a placeholder patch. Add soft async catcher Must be enabled via -Dtuinity.strict-thread-checks=true Delay chunk unloads Chunk unloads are now delayed by 1s. Specifically, ticket level reduction is delayed by 1s. This is done to allow players to teleport and have their pets follow them, as the chunks will no longer unload or have entity ticking status removed. It's also targetted to reduce performance regressions when plugins or edge cases in code do not spam sync loads since chunks without tickets get unloaded immediately. Configurable under `delay-chunkunloads-by` in config. This patch replaces the paper patch as the paper patch only affects player loaded chunks, when we want to target all loads. Attempt to recalculate regionfile header if it is corrupt Instead of trying to relocate the chunk, which is seems to never be the correct choice, so we end up duplicating or swapping chunks, we instead drop the current regionfile header and recalculate - hoping that at least then we don't swap chunks, and maybe recover them all. Lag compensate block breaking Use time instead of ticks if ticks fall behind Update version fetcher repo Sets the target github repo to Tuinity in the version checker. Also disables the jenkins build lookups. Per World Spawn Limits Detail more information in watchdog dumps - Dump position, world, velocity, and uuid for currently ticking entities - Dump player name, player uuid, position, and world for packet handling Execute chunk tasks mid-tick This will help the server load chunks if tick times are high. Change writes to use NORMAL priority rather than LOW Should limit build up of I/O tasks, or at least properly indicate to server owners that I/O is falling behind Allow controlled flushing for network manager Only make one flush call when emptying the packet queue too This patch will be used to optimise out flush calls in later patches. Consolidate flush calls for entity tracker packets Most server packets seem to be sent from here, so try to avoid expensive flush calls from them. This change was motivated due to local testing: - My server spawn has 130 cows in it (for testing a prev. patch) - Try to let 200 players join spawn Without this change, I could only get 20 players on before they all started timing out due to the load put on the Netty I/O threads. With this change I could get all 200 on at 0ms ping. (one of the primary issues is that my CPU is kinda trash, and having 4 extra threads at 100% is just too much for it). So in general this patch should reduce Netty I/O thread load. Time scoreboard search Plugins leaking scoreboards will make this very expensive, let server owners debug it easily Make CallbackExecutor strict again The correct fix for double scheduling is to avoid it. The reason this class is used is because double scheduling causes issues elsewhere, and it acts as an explicit detector of what double schedules. Effectively, use the callback executor as a tool of finding issues rather than hiding these issues. This patch also reverts incorrect use(s) of the class by paper. - getChunkFutureAsynchronously There is no risk at all of recursion. The future is executed on the chunk provider's thread queue, the same place general plugin load callbacks are executed on. Forcing the task execution into the callback executor also prevents the future from catching any exception thrown from it. Optimise entity hard collision checking Very few entities actually hard collide, so store them in their own entity slices and provide a special getEntites type call just for them. This reduces entity collision checking impact (in my testing) by 25% for crammed entities (shove 130 cows into an 8x6 area in one chunk). Less crammed entities are likely to show significantly less benefit. Effectively, this patch optimises crammed entity situations. Improved oversized chunk data packet handling Now target all TE data, except for TE's that do not have update packets. This patch relies upon the improve extra packet handling patch, as we now use PacketPlayOutMapChunk as an extra packet. See its patch notes for further details. Reduce iterator allocation from chunk gen Replace via iterating over an array Prevent long map entry creation in light engine Use fastiterator to prevent it Highly optimise single and multi-AABB VoxelShapes and collisions Reduce allocation rate from crammed entities Optimise chunk tick iteration Use a dedicated list of entity ticking chunks to reduce the cost Use entity ticking chunk map for entity tracker Should bring us back in-line with tracker performance before the loaded entity list reversion. Improve paper prevent moving into unloaded chunk check Check the AABB of the move Improve async tp to not load chunks when crossing worlds Fixes an issue where a getCubes call would load neighbouring chunks. Loads less chunks than paper's implementation Revert getChunkAt(Async) retaining chunks for long periods of time Rework PlayerChunk main thread checks These need to fail instead of continuing, as hiding these errors the way paper has is just going to allow unexpected reordering of callbacks. For example, thanks to this patch incorrect future completion (completion of the world gen future, PlayerChunkMap#b(PlayerChunk, ChunkStatus)) was detected and fixed. Allow Entities to be removed from a world while ticking Fixes issues like disconnecting players while ticking them, or issues where teleporting players across worlds while ticking. Also allows us to run mid tick while ticking entities. Prevent unload() calls removing tickets for sync loads Prevent log spam for "Block is water but TE is chest" Happens when breaking a waterlogged chest. Fix is to just not validate the TE while the chest is being removed. Optimise collision checking in player move packet handling Don't need to do another getCubes call if the move() call doesn't find any collisions Manually inline methods in BlockPosition Separate lookup locking from state access in UserCache Prevent lookups from stalling simple state access/write calls Distance manager tick timings Recently this has been taking up more time, so add a timings to really figure out how much. Name craft scheduler threads according to the plugin using them Provides quick access to culprits running far more threads than they should be Fix swamp hut cat generation deadlock The worldgen thread will attempt to get structure references via the world's getChunkAt method, which is fine if the gen is not cancelled - but if the chunk was unloaded, the call will block indefinitely. Instead of using the world state, we use the already supplied generatoraccess which will always have the chunk available. Range check flag dirty calls in PlayerChunk Simply return. Optimise tab complete Some of the toLowerCase calls can be expensive. Do not allow ticket level changes while unloading playerchunks Sync loading the chunk at this stage would cause it to load older data, as well as screwing our region state. Make sure inlined getChunkAt has inlined logic for loaded chunks Tux did some profiling some time ago and showed that the previous getChunkAt method which had inlined logic for loaded chunks did get inlined, but the standard CPS.getChunkAt method was not inlined. Paper recently reverted this optimisation, so it's been reintroduced here. Temporarily Revert usage of Region Manager Has some stability issues. Add packet limiter config Example config: packet-limiter: kick-message: '&cSent too many packets' limits: all: interval: 7.0 max-packet-rate: 500.0 PacketPlayInAutoRecipe: interval: 4.0 max-packet-rate: 5.0 action: DROP all section refers to all incoming packets, the action for all is hard coded to KICK. For specific limits, the section name is the class's name, and an action can be defined: DROP or KICK If interval or rate are less-than 0, the limit is ignored Optimise closest entity lookup used by AI goals Use a special entity slice for tracking entities by class as well as counts per chunk. This should reduce the number of entities searched. Optimise EntityInsentient#checkDespawn Use a distance map to map out close players. Note that it's important that we cache the distance map value per chunk since the penalty of a map lookup could outweigh the benefits of searching less players (as it basically did in the outside range patch). Remove streams for villager AI POI searching: Turns out chaining a lot of streams together has inane amounts of overheard. Use the good ol iterator method to remove that overhead. The rest is just standard stream removal. Also remove streams for poi searching in some zombie pathfinding. Don't lookup fluid state when raytracing Just use the iblockdata already retrieved, removes a getType call. Reduce pathfinder branches Reduce static path type detection to simple lazy-init fields Add Velocity natives for encryption and compression Optimise non-flush packet sending Places like entity tracking make heavy use of packet sending, and internally netty will use some very expensive thread wakeup calls when scheduling. Thanks to various hacks in ProtocolLib as well as other plugins, we cannot simply use a queue of packets to group send on execute. We have to call execute for each packet. Tux's suggestion here is exactly what was needed - tag the Runnable indicating it should not make a wakeup call. Big thanks to Tux for making this possible as I had given up on this optimisation before he came along. Locally this patch drops the entity tracker tick by a full 1.5x. Do not retain playerchunkmap instance in light thread factory The executor returned is finalizable and of course that causes issues. Do not load chunks during a crash report This causes deadlocks in some cases when generating crash reports. Fixes https://github.com/Spottedleaf/Tuinity/issues/215 Improve abnormal server shutdown process - When we're trying to kill the main thread from watchdog, step up the stop() spamming after 15s to really kill the main thread. - Do not wait for window disposing when disposing of the server gui. It looks like during sigint shutdown there can be some deadlock between the server thread and awt shutdown thread here. Copy passenger list in enderTeleportTo Fixes https://github.com/Spottedleaf/Tuinity/issues/208 Revert MC-4 fix When messing around with collisions, I ran into problems where entity position was off by ULP and that caused clipping problems. Now, the collision epsilon is 1.0e-7 to account for those errors. But this patch is going to cause problems on the order of 1.0e-4. I do not want to deal with clipping problems. The very fact it works shows it's causing the clipping to occur serverside. Prevent light queue overfill when no players are online block changes don't queue light updates (and they shouldn't) Do not add passengers of entities that were were above save limit Given that the root entity isn't added to the world, this is pretty unsafe to do. Rewrite the light engine The standard vanilla light engine is plagued by awful performance. Paper's changes to the light engine help a bit, however they appear to cause some lighting errors - most easily noticed in coral generation. The vanilla light engine's is too abstract to be modified - so an entirely new implementation is required to fix the performance and lighting errors. The new implementation is designed primarily to optimise light level propagations (increase and decrease). Unlike the vanilla light engine, this implementation tracks more information per queued value when performing a breadth first search. Vanilla just tracks coordinate, which means every time they handle a queued value, they must also determine the coordinate's target light level from its neighbours - very wasteful, especially considering these checks read neighbour block data. The new light engine tracks both position and target level, as well as whether the target block needs to be read at all (for checking sided propagation). So, the work done per coordinate is significantly reduced because no work is done for calculating the target level. In my testing, the block get calls were reduced by approximately an order of magnitude. However, the light read checks were only reduced by approximately 2x - but this is fine, light read checks are extremely cheap compared to block gets. Generation testing showed that the new light engine improved total generation (not lighting itself, but the whole generation process) by 2x. According to cpu time, the light engine itself spent 10x less time lighting chunks for generation. fixup! Highly optimise single and multi-AABB VoxelShapes and collisions Revert Temporarily-Revert-usage-of-Region-Manager Optimise WorldServer#notify Iterating over all of the navigators in the world is pretty expensive. Instead, only iterate over navigators in the current region that are eligible for repathing. fixup! Util patch fixup! Util patch Actually unload POI data While it's not likely for a poi data leak to be meaningful, sometimes it is. This patch also prevents the saving/unloading of POI data when world saving is disabled. diff --git a/pom.xml b/pom.xml index 9ba379b7e3ee3bc8c6d2c8ec46213c404c73d682..e83e4241a56fe131a75fe21cc1518992c089da2c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ 4.0.0 - paper + tuinity jar 1.16.5-R0.1-SNAPSHOT - Paper - https://papermc.io + Tuinity-Server + https://github.com/Spottedleaf/Tuinity @@ -19,16 +19,16 @@ - com.destroystokyo.paper - paper-parent + com.tuinity + tuinity-parent dev-SNAPSHOT ../pom.xml - com.destroystokyo.paper - paper-api + com.tuinity + tuinity-api ${project.version} compile @@ -38,6 +38,13 @@ ${project.version} compile + + + io.netty + netty-all + 4.1.50.Final + + io.papermc minecraft-server @@ -105,11 +112,7 @@ cleaner 1.0-SNAPSHOT - - io.netty - netty-all - 4.1.50.Final - + com.googlecode.json-simple @@ -149,6 +152,13 @@ 4.8.47 test + + + com.velocitypowered + velocity-native + 1.1.0-SNAPSHOT + compile + @@ -173,15 +183,15 @@ - paper-${minecraft.version} - clean install + tuinity-${minecraft.version} + install com.lukegb.mojo gitdescribe-maven-plugin 1.3 - git-Paper- + git-Tuinity- .. diff --git a/src/main/java/co/aikar/timings/MinecraftTimings.java b/src/main/java/co/aikar/timings/MinecraftTimings.java index 884b59d478aa7de49906520e77866a7949bed19d..68ab5ccb2fcfe1de0503c9336572f28e11832b2d 100644 --- a/src/main/java/co/aikar/timings/MinecraftTimings.java +++ b/src/main/java/co/aikar/timings/MinecraftTimings.java @@ -43,6 +43,9 @@ public final class MinecraftTimings { public static final Timing antiXrayUpdateTimer = Timings.ofSafe("anti-xray - update"); public static final Timing antiXrayObfuscateTimer = Timings.ofSafe("anti-xray - obfuscate"); + public static final Timing scoreboardScoreSearch = Timings.ofSafe("Scoreboard score search"); // Tuinity - add timings for scoreboard search + public static final Timing distanceManagerTick = Timings.ofSafe("Distance Manager Tick"); // Tuinity - add timings for distance manager + private static final Map, String> taskNameCache = new MapMaker().weakKeys().makeMap(); private MinecraftTimings() {} diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java index e33e889c291d37a821a4fbd40d9aac7bb079de0d..5dfa0658838c4801cdf260eae8b98163f729e5af 100644 --- a/src/main/java/co/aikar/timings/TimingsExport.java +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -229,7 +229,8 @@ public class TimingsExport extends Thread { parent.put("config", createObject( pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)), pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)), - pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)) + pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)), // Tuinity - add config to timings report + pair("tuinity", mapAsJSON(Bukkit.spigot().getTuinityConfig(), null)) // Tuinity - add config to timings report )); new TimingsExport(listeners, parent, history).start(); diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java index dee00aac05f1acf050f05d4db557a08dd0f301c8..52c0ab1ce46e1f3233ef746d9bc699356fa9fae4 100644 --- a/src/main/java/com/destroystokyo/paper/Metrics.java +++ b/src/main/java/com/destroystokyo/paper/Metrics.java @@ -593,7 +593,7 @@ public class Metrics { boolean logFailedRequests = config.getBoolean("logFailedRequests", false); // Only start Metrics, if it's enabled in the config if (config.getBoolean("enabled", true)) { - Metrics metrics = new Metrics("Paper", serverUUID, logFailedRequests, Bukkit.getLogger()); + Metrics metrics = new Metrics("Tuinity", serverUUID, logFailedRequests, Bukkit.getLogger()); // Tuinity - we have our own bstats page metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { String minecraftVersion = Bukkit.getVersion(); @@ -603,7 +603,7 @@ public class Metrics { metrics.addCustomChart(new Metrics.SingleLineChart("players", () -> Bukkit.getOnlinePlayers().size())); metrics.addCustomChart(new Metrics.SimplePie("online_mode", () -> Bukkit.getOnlineMode() || PaperConfig.isProxyOnlineMode() ? "online" : "offline")); - metrics.addCustomChart(new Metrics.SimplePie("paper_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); + metrics.addCustomChart(new Metrics.SimplePie("tuinity_version", () -> (Metrics.class.getPackage().getImplementationVersion() != null) ? Metrics.class.getPackage().getImplementationVersion() : "unknown")); // Tuinity - we have our own bstats page metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { Map> map = new HashMap<>(); diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java index 0abfe19e204d20af0f8dedbeedb0ef98dfe9d3c8..1a876336384198bad2a25c018be5f2418027dd47 100644 --- a/src/main/java/com/destroystokyo/paper/PaperCommand.java +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -186,6 +186,44 @@ public class PaperCommand extends Command { } } + private void starlightFixLight(EntityPlayer sender, WorldServer world, LightEngineThreaded lightengine, int radius) { + long start = System.nanoTime(); + LinkedHashSet chunks = new LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.getChunkCoordinates(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos + + int[] pending = new int[1]; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkCoordIntPair chunkPos = iterator.next(); + + final IChunkAccess chunk = world.getChunkProvider().getChunkAtImmediately(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLit() || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + ++pending[0]; + } + + int[] relitChunks = new int[1]; + lightengine.relight(chunks, + (ChunkCoordIntPair chunkPos) -> { + ++relitChunks[0]; + sender.getBukkitEntity().sendMessage( + ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE + + ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%" + ); + }, + (int totalRelit) -> { + final long end = System.nanoTime(); + final long diff = Math.round(1.0e-6*(end - start)); + sender.getBukkitEntity().sendMessage( + ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " + + ChatColor.DARK_AQUA + diff + "ms" + ); + }); + sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks"); + } + private void doFixLight(CommandSender sender, String[] args) { if (!(sender instanceof Player)) { sender.sendMessage("Only players can use this command"); @@ -194,7 +232,7 @@ public class PaperCommand extends Command { int radius = 2; if (args.length > 1) { try { - radius = Math.min(5, Integer.parseInt(args[1])); + radius = Math.min(15, Integer.parseInt(args[1])); // Tuinity - MOOOOOORE } catch (Exception e) { sender.sendMessage("Not a number"); return; @@ -207,6 +245,13 @@ public class PaperCommand extends Command { net.minecraft.server.WorldServer world = (WorldServer) handle.world; LightEngineThreaded lightengine = world.getChunkProvider().getLightEngine(); + // Tuinity start - rewrite light engine + if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) { + this.starlightFixLight(handle, world, lightengine, radius); + return; + } + // Tuinity end - rewrite light engine + BlockPosition center = MCUtil.toBlockPosition(player.getLocation()); Deque queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius)); updateLight(sender, world, lightengine, queue); diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java index 49a38c6608b652ff48ef4eaca0dd3ccb1ba570e3..255bbd6e48b95c70fad02ba692c64c7579496827 100644 --- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java @@ -24,8 +24,8 @@ public class PaperVersionFetcher implements VersionFetcher { @Nonnull @Override public String getVersionMessage(@Nonnull String serverVersion) { - String[] parts = serverVersion.substring("git-Paper-".length()).split("[-\\s]"); - String updateMessage = getUpdateStatusMessage("PaperMC/Paper", GITHUB_BRANCH_NAME, parts[0]); + String[] parts = serverVersion.substring("git-Tuinity-".length()).split("[-\\s]"); // Tuinity + String updateMessage = getUpdateStatusMessage("Spottedleaf/Tuinity", GITHUB_BRANCH_NAME, parts[0]); // Tuinity String history = getHistory(); return history != null ? history + "\n" + updateMessage : updateMessage; @@ -49,13 +49,10 @@ public class PaperVersionFetcher implements VersionFetcher { private static String getUpdateStatusMessage(@Nonnull String repo, @Nonnull String branch, @Nonnull String versionInfo) { int distance; - try { - int jenkinsBuild = Integer.parseInt(versionInfo); - distance = fetchDistanceFromSiteApi(jenkinsBuild, getMinecraftVersion()); - } catch (NumberFormatException ignored) { + // Tuinity - we don't have jenkins setup versionInfo = versionInfo.replace("\"", ""); distance = fetchDistanceFromGitHub(repo, branch, versionInfo); - } + // Tuinity - we don't have jenkins setup switch (distance) { case -1: diff --git a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java index e7624948ea4aa1a07d84ed3d295cfe2dd354fd14..77df6888803093ad9527d276033f2ed767b39764 100644 --- a/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java +++ b/src/main/java/com/destroystokyo/paper/server/ticklist/PaperTickList.java @@ -186,6 +186,7 @@ public final class PaperTickList extends TickListServer { // extend to avo } public void onChunkSetTicking(final int chunkX, final int chunkZ) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list chunk ticking update"); // Tuinity - soft async catcher final ArrayList> pending = this.pendingChunkTickLoad.remove(MCUtil.getCoordinateKey(chunkX, chunkZ)); if (pending == null) { return; @@ -268,6 +269,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override protected void nextTick() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list tick"); // Tuinity - soft async catcher ++this.currentTick; if (this.currentTick != this.world.getTime()) { if (!this.warnedAboutDesync) { @@ -280,6 +282,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override public void tick() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list tick"); // Tuinity - soft async catcher final ChunkProviderServer chunkProvider = this.world.getChunkProvider(); this.world.getMethodProfiler().enter("cleaning"); @@ -307,6 +310,7 @@ public final class PaperTickList extends TickListServer { // extend to avo if (toTick.tickState == STATE_TICKING) { toTick.tickState = STATE_TICKED; } // else it's STATE_CANCELLED_TICK + MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick } else { // re-schedule eventually toTick.tickState = STATE_SCHEDULED; @@ -424,6 +428,7 @@ public final class PaperTickList extends TickListServer { // extend to avo } public void schedule(final BlockPosition pos, final T data, final long targetTick, final TickListPriority priority) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list schedule"); // Tuinity - soft async catcher final NextTickListEntry entry = new NextTickListEntry<>(pos, data, targetTick, priority); if (this.excludeFromScheduling.test(entry.getData())) { return; @@ -479,6 +484,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override public List> getEntriesInBoundingBox(final StructureBoundingBox structureboundingbox, final boolean removeReturned, final boolean excludeTicked) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list get in bounding box"); // Tuinity - soft async catcher if (structureboundingbox.getMinX() == structureboundingbox.getMaxX() || structureboundingbox.getMinZ() == structureboundingbox.getMaxZ()) { return Collections.emptyList(); // vanilla behaviour, check isBlockInSortof above } @@ -535,6 +541,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override public void copy(StructureBoundingBox structureboundingbox, BlockPosition blockposition) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list copy"); // Tuinity - soft async catcher // start copy from TickListServer // TODO check on update List> list = this.getEntriesInBoundingBox(structureboundingbox, false, false); Iterator> iterator = list.iterator(); @@ -554,6 +561,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override public List> getEntriesInChunk(ChunkCoordIntPair chunkPos, boolean removeReturned, boolean excludeTicked) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list get"); // Tuinity - soft async catcher // Vanilla DOES get the entries 2 blocks out of the chunk too, but that doesn't matter since we ignore chunks // not at ticking status, and ticking status requires neighbours loaded // so with this method we will reduce scheduler churning @@ -585,6 +593,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override public NBTTagList serialize(ChunkCoordIntPair chunkcoordintpair) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list serialize"); // Tuinity - soft async catcher // start copy from TickListServer // TODO check on update List> list = this.getEntriesInChunk(chunkcoordintpair, false, true); @@ -594,6 +603,7 @@ public final class PaperTickList extends TickListServer { // extend to avo @Override public int getTotalScheduledEntries() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async tick list get size"); // Tuinity - soft async catcher // good thing this is only used in debug reports // TODO check on update int ret = 0; diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index 103576715ef6ae4df4b216ae9ae31b6fb1086bd5..e8fdbe7b8d8192a3247d98534e78ede7a7314a91 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -590,10 +590,11 @@ public class CommandDispatcher { final String truncatedInput = fullInput.substring(0, cursor); @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[parent.getChildren().size()]; int i = 0; + final String remainingLower = truncatedInput.substring(start).toLowerCase(); // Tuinity for (final CommandNode node : parent.getChildren()) { CompletableFuture future = Suggestions.empty(); try { - future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start)); + future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start, remainingLower)); // Tuinity } catch (final CommandSyntaxException ignored) { } futures[i++] = future; diff --git a/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java b/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java index cb993ca102402d9c19ea9fa04e5db09c21205896..849686f7b2a8b0044f7cd14c8c2e401e80966462 100644 --- a/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java +++ b/src/main/java/com/mojang/brigadier/arguments/BoolArgumentType.java @@ -34,10 +34,10 @@ public class BoolArgumentType implements ArgumentType { @Override public CompletableFuture listSuggestions(final CommandContext context, final SuggestionsBuilder builder) { - if ("true".startsWith(builder.getRemaining().toLowerCase())) { + if ("true".startsWith(builder.getRemainingLowercase())) { // Tuinity builder.suggest("true"); } - if ("false".startsWith(builder.getRemaining().toLowerCase())) { + if ("false".startsWith(builder.getRemainingLowercase())) { // Tuinity builder.suggest("false"); } return builder.buildFuture(); diff --git a/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java b/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java index bc0024adb804ac055a4f8afb7f85d00ec13931e9..0343f6663c450c3f0d9c57d817eef9c979055939 100644 --- a/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java +++ b/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java @@ -14,9 +14,16 @@ public class SuggestionsBuilder { private final String input; private final int start; private final String remaining; + private String remainingLowercase; public final String getRemainingLowercase() { return this.remainingLowercase == null ? this.remainingLowercase = this.remaining.toLowerCase() : this.remainingLowercase; } // Tuinity private final List result = new ArrayList<>(); public SuggestionsBuilder(final String input, final int start) { + // Tuinity start + this(input, start, null); + } + public SuggestionsBuilder(final String input, final int start, final String remainingLowercase) { + this.remainingLowercase = remainingLowercase; + // Tuinity end this.input = input; this.start = start; this.remaining = input.substring(start); diff --git a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java index 7720578796e28d28e8c0c9aa40155cd205c17d54..e5db29d4cadb5702c7d06b0b6e2d05586a652ec8 100644 --- a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java @@ -20,11 +20,11 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; public class LiteralCommandNode extends CommandNode { - private final String literal; + private final String literal; private final String literalLower; // Tuinity public LiteralCommandNode(final String literal, final Command command, final Predicate requirement, final CommandNode redirect, final RedirectModifier modifier, final boolean forks) { super(command, requirement, redirect, modifier, forks); - this.literal = literal; + this.literal = literal; this.literalLower = this.literal.toLowerCase(); // Tuinity } public String getLiteral() { @@ -66,7 +66,7 @@ public class LiteralCommandNode extends CommandNode { @Override public CompletableFuture listSuggestions(final CommandContext context, final SuggestionsBuilder builder) { - if (literal.toLowerCase().startsWith(builder.getRemaining().toLowerCase())) { + if (literalLower.startsWith(builder.getRemainingLowercase())) { // Tuinity return builder.suggest(literal).buildFuture(); } else { return Suggestions.empty(); diff --git a/src/main/java/com/tuinity/tuinity/chunk/ChunkEntitiesByClass.java b/src/main/java/com/tuinity/tuinity/chunk/ChunkEntitiesByClass.java new file mode 100644 index 0000000000000000000000000000000000000000..37428f4b9ae45175fda545e9d8b55cf8a3b8c87b --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/ChunkEntitiesByClass.java @@ -0,0 +1,186 @@ +package com.tuinity.tuinity.chunk; + +import com.destroystokyo.paper.util.maplist.EntityList; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.Chunk; +import net.minecraft.server.Entity; +import net.minecraft.server.MathHelper; +import org.spigotmc.AsyncCatcher; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public final class ChunkEntitiesByClass { + + // this class attempts to restore the original intent of nms.EntitySlice and improve upon it: + // fast lookups for specific entity types in a chunk. However vanilla does not track things on a + // chunk-wide basis, which is very important to our optimisations here: we want to eliminate chunks + // before searching multiple slices. We also want to maintain only lists that we need to maintain for memory purposes: + // so we have no choice but to lazily initialise mappings of class -> entity. + // Typically these are used for entity AI lookups, which means we take a heavy initial cost but ultimately win + // since AI lookups happen a lot. + + // This optimisation is only half of the battle with entity AI, we need to be smarter about picking the closest entity. + // See World#getClosestEntity + + // aggressively high load factors for each map here + fastutil collections: we want the smallest memory footprint + private final ExposedReference2IntOpenHashMap> chunkWideCount = new ExposedReference2IntOpenHashMap<>(4, 0.9f); + { + this.chunkWideCount.defaultReturnValue(Integer.MIN_VALUE); + } + private final Reference2ObjectOpenHashMap, ArrayList>[] slices = new Reference2ObjectOpenHashMap[16]; + private final Chunk chunk; + + public ChunkEntitiesByClass(final Chunk chunk) { + this.chunk = chunk; + } + + public boolean hasEntitiesMaybe(final Class clazz) { + final int count = this.chunkWideCount.getInt(clazz); + return count == Integer.MIN_VALUE || count > 0; + } + + public void addEntity(final Entity entity, final int sectionY) { + AsyncCatcher.catchOp("Add entity call"); + if (this.chunkWideCount.isEmpty()) { + return; + } + + final Object[] keys = this.chunkWideCount.getKey(); + final int[] values = this.chunkWideCount.getValue(); + + Reference2ObjectOpenHashMap, ArrayList> slice = this.slices[sectionY]; + if (slice == null) { + slice = this.slices[sectionY] = new Reference2ObjectOpenHashMap<>(4, 0.9f); + } + + for (int i = 0, len = keys.length; i < len; ++i) { + final Object _key = keys[i]; + if (!(_key instanceof Class)) { + continue; + } + final Class key = (Class)_key; + if (key.isInstance(entity)) { + ++values[i]; + slice.computeIfAbsent(key, (keyInMap) -> { + return new ArrayList<>(); + }).add(entity); + } + } + } + + public void removeEntity(final Entity entity, final int sectionY) { + AsyncCatcher.catchOp("Remove entity call"); + if (this.chunkWideCount.isEmpty()) { + return; + } + + final Object[] keys = this.chunkWideCount.getKey(); + final int[] values = this.chunkWideCount.getValue(); + + Reference2ObjectOpenHashMap, ArrayList> slice = this.slices[sectionY]; + if (slice == null) { + return; // seriously brain damaged plugins + } + + for (int i = 0, len = keys.length; i < len; ++i) { + final Object _key = keys[i]; + if (!(_key instanceof Class)) { + continue; + } + final Class key = (Class)_key; + if (key.isInstance(entity)) { + --values[i]; + final ArrayList list = slice.get(key); + if (list == null) { + return; // seriously brain damaged plugins + } + list.remove(entity); + } + } + } + + + private void computeClass(final Class clazz) { + AsyncCatcher.catchOp("Entity class compute call"); + int totalCount = 0; + + EntityList entityList = this.chunk.entities; + Entity[] entities = entityList.getRawData(); + for (int i = 0, len = entityList.size(); i < len; ++i) { + final Entity entity = entities[i]; + + if (clazz.isInstance(entity)) { + ++totalCount; + Reference2ObjectOpenHashMap, ArrayList> slice = this.slices[entity.chunkY]; + if (slice == null) { + slice = this.slices[entity.chunkY] = new Reference2ObjectOpenHashMap<>(4, 0.9f); + } + slice.computeIfAbsent(clazz, (keyInMap) -> { + return new ArrayList<>(); + }).add(entity); + } + } + + this.chunkWideCount.put(clazz, totalCount); + } + + public void lookupClass(final Class clazz, final Entity entity, final AxisAlignedBB boundingBox, final Predicate predicate, final List into) { + final int count = this.chunkWideCount.getInt(clazz); + if (count == Integer.MIN_VALUE) { + this.computeClass(clazz); + if (this.chunkWideCount.getInt(clazz) <= 0) { + return; + } + } else if (count <= 0) { + return; + } + + // copied from getEntities + int min = MathHelper.floor((boundingBox.minY - 2.0D) / 16.0D); + int max = MathHelper.floor((boundingBox.maxY + 2.0D) / 16.0D); + + min = MathHelper.clamp(min, 0, this.slices.length - 1); + max = MathHelper.clamp(max, 0, this.slices.length - 1); + + for (int y = min; y <= max; ++y) { + final Reference2ObjectOpenHashMap, ArrayList> slice = this.slices[y]; + if (slice == null) { + continue; + } + + final ArrayList entities = slice.get(clazz); + if (entities == null) { + continue; + } + + for (int i = 0, len = entities.size(); i < len; ++i) { + Entity entity1 = entities.get(i); + if (entity1.shouldBeRemoved) continue; // Paper + + if (entity1 != entity && entity1.getBoundingBox().intersects(boundingBox)) { + if (predicate == null || predicate.test(entity1)) { + into.add(entity1); + } + } + } + } + } + + static final class ExposedReference2IntOpenHashMap extends Reference2IntOpenHashMap { + + public ExposedReference2IntOpenHashMap(final int expected, final float loadFactor) { + super(expected, loadFactor); + } + + public Object[] getKey() { + return this.key; + } + + public int[] getValue() { + return this.value; + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..cae06962d80cdd00962236891472ba815b0ab8cd --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/SingleThreadChunkRegionManager.java @@ -0,0 +1,491 @@ +package com.tuinity.tuinity.chunk; + +import co.aikar.timings.MinecraftTimings; +import co.aikar.timings.Timing; +import com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.ChunkCoordIntPair; +import net.minecraft.server.MCUtil; +import net.minecraft.server.WorldServer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +public final class SingleThreadChunkRegionManager & SingleThreadChunkRegionManager.RegionDataCreator> { + + static final int REGION_SECTION_MERGE_RADIUS = 1; + // if this becomes > 8, then the RegionSection needs to be properly modified (see bitset) + public static final int REGION_CHUNK_SIZE = 32; + public static final int REGION_CHUNK_SIZE_SHIFT = 5; // log2(REGION_CHUNK_SIZE) + + public final WorldServer world; + public final Class dataClass; + public final String name; + + public final Timing addChunkTimings; + public final Timing removeChunkTimings; + public final Timing regionRecalculateTimings; + + protected final Long2ObjectOpenHashMap> regionsBySection = new Long2ObjectOpenHashMap<>(); + protected final ReferenceLinkedOpenHashSet> needsRecalculation = new ReferenceLinkedOpenHashSet<>(); + protected final int minSectionRecalcCount; + protected final double maxDeadRegionPercent; + + public SingleThreadChunkRegionManager(final WorldServer world, final Class enumClass, + final int minSectionRecalcCount, final double maxDeadRegionPercent, + final String name) { + this.world = world; + this.dataClass = enumClass; + this.name = name; + this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); + this.maxDeadRegionPercent = maxDeadRegionPercent; + + String prefix = world.getWorld().getName() + " - Region Manager - " + name + " - "; + this.addChunkTimings = MinecraftTimings.getInternalTaskName(prefix.concat("add")); + this.removeChunkTimings = MinecraftTimings.getInternalTaskName(prefix.concat("remove")); + this.regionRecalculateTimings = MinecraftTimings.getInternalTaskName(prefix.concat("recalculate")); + } + + // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f + + /* + protected void check() { + ReferenceOpenHashSet> checked = new ReferenceOpenHashSet<>(); + + for (RegionSection section : this.regionsBySection.values()) { + if (!checked.add(section.region)) { + section.region.check(); + } + } + for (Region region : this.needsRecalculation) { + region.check(); + } + } + */ + + protected void addToRecalcQueue(final Region region) { + this.needsRecalculation.add(region); + } + + protected void removeFromRecalcQueue(final Region region) { + this.needsRecalculation.remove(region); + } + + public RegionSection getRegionSection(final int chunkX, final int chunkZ) { + return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> REGION_CHUNK_SIZE_SHIFT, chunkZ >> REGION_CHUNK_SIZE_SHIFT)); + } + + public Region getRegion(final int chunkX, final int chunkZ) { + final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> REGION_CHUNK_SIZE_SHIFT, chunkZ >> REGION_CHUNK_SIZE_SHIFT)); + return section != null ? section.region : null; + } + + private final List> toMerge = new ArrayList<>((2 * REGION_SECTION_MERGE_RADIUS + 1) * (2 * REGION_SECTION_MERGE_RADIUS + 1)); + + protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) { + final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ); + + if (force == null) { + RegionSection region = this.regionsBySection.get(sectionKey); + if (region != null) { + return region; + } + } + + int mergeCandidateSectionSize = -1; + Region mergeIntoCandidate = null; + + // find optimal candidate to merge into + + final int minX = sectionX - REGION_SECTION_MERGE_RADIUS; + final int maxX = sectionX + REGION_SECTION_MERGE_RADIUS; + final int minZ = sectionZ - REGION_SECTION_MERGE_RADIUS; + final int maxZ = sectionZ + REGION_SECTION_MERGE_RADIUS; + for (int currX = minX; currX <= maxX; ++currX) { + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ)); + if (section == null) { + continue; + } + final Region region = section.region; + if (region.dead) { + throw new IllegalStateException("Dead region should not be in live region manager state: " + region); + } + final int sections = region.sections.size(); + + if (sections > mergeCandidateSectionSize) { + mergeCandidateSectionSize = sections; + mergeIntoCandidate = region; + } + this.toMerge.add(region); + } + } + + // merge + if (mergeIntoCandidate != null) { + for (int i = 0; i < this.toMerge.size(); ++i) { + final Region region = this.toMerge.get(i); + if (region.dead || mergeIntoCandidate == region) { + continue; + } + region.mergeInto(mergeIntoCandidate); + } + this.toMerge.clear(); + } else { + mergeIntoCandidate = new Region<>(this); + } + + final RegionSection section; + if (force == null) { + this.regionsBySection.put(sectionKey, section = new RegionSection<>(sectionKey, this)); + } else { + final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force); + if (existing != null) { + throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() + + ", with " + force.toStringWithRegion()); + } + + section = force; + } + + section.region = mergeIntoCandidate; + mergeIntoCandidate.sections.add(section); + //mergeIntoCandidate.check(); + //this.check(); + + return section; + } + + public void addChunk(final int chunkX, final int chunkZ) { + com.tuinity.tuinity.util.TickThread.ensureTickThread("async region manager add chunk"); // Tuinity + this.addChunkTimings.startTiming(); + try { + this.getOrCreateAndMergeSection(chunkX >> REGION_CHUNK_SIZE_SHIFT, chunkZ >> REGION_CHUNK_SIZE_SHIFT, null).addChunk(chunkX, chunkZ); + } finally { + this.addChunkTimings.stopTiming(); + } + } + + public void removeChunk(final int chunkX, final int chunkZ) { + com.tuinity.tuinity.util.TickThread.ensureTickThread("async region manager remove chunk"); // Tuinity + this.removeChunkTimings.startTiming(); + try { + final RegionSection section = this.regionsBySection.get( + MCUtil.getCoordinateKey(chunkX >> REGION_CHUNK_SIZE_SHIFT, chunkZ >> REGION_CHUNK_SIZE_SHIFT) + ); + if (section != null) { + section.removeChunk(chunkX, chunkZ); + } else { + throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist"); + } + } finally { + this.removeChunkTimings.stopTiming(); + } + } + + public void recalculateRegions() { + com.tuinity.tuinity.util.TickThread.ensureTickThread("async region recalculation"); // Tuinity + for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) { + final Region region = this.needsRecalculation.removeFirst(); + + this.recalculateRegion(region); + //this.check(); + } + } + + protected void recalculateRegion(final Region region) { + this.regionRecalculateTimings.startTiming(); + try { + region.markedForRecalc = false; + //region.check(); + // clear unused regions + for (final Iterator> iterator = region.deadSections.iterator(); iterator.hasNext();) { + final RegionSection deadSection = iterator.next(); + + if (deadSection.hasChunks()) { + throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); + } + if (!region.sections.remove(deadSection)) { + throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); + } + if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) { + throw new IllegalStateException("Cannot remove dead section '" + + deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + + this.regionsBySection.get(deadSection.regionCoordinate)); + } + } + region.deadSections.clear(); + + // implicitly cover cases where size == 0 + if (region.sections.size() < this.minSectionRecalcCount) { + //region.check(); + return; + } + + // run a test to see if we actually need to recalculate + // TODO + + // destroy and rebuild the region + region.dead = true; + + // destroy region state + for (final Iterator> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection aliveSection = iterator.next(); + if (!aliveSection.hasChunks()) { + throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!"); + } + if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) { + throw new IllegalStateException("Cannot remove alive section '" + + aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " + + this.regionsBySection.get(aliveSection.regionCoordinate)); + } + } + + // rebuild regions + for (final Iterator> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection aliveSection = iterator.next(); + this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection); + } + } finally { + this.regionRecalculateTimings.stopTiming(); + } + } + + public static final class Region & SingleThreadChunkRegionManager.RegionDataCreator> { + protected final IteratorSafeOrderedReferenceSet> sections = new IteratorSafeOrderedReferenceSet<>(true); + protected final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(16, 0.7f); + protected boolean dead; + protected boolean markedForRecalc; + + public final SingleThreadChunkRegionManager regionManager; + + protected Region(final SingleThreadChunkRegionManager regionManager) { + this.regionManager = regionManager; + } + + public IteratorSafeOrderedReferenceSet.Iterator> getSections() { + return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); + } + + protected final double getDeadSectionPercent() { + return (double)this.deadSections.size() / (double)this.sections.size(); + } + + /* + protected void check() { + if (this.dead) { + throw new IllegalStateException("Dead region!"); + } + for (final Iterator> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection section = iterator.next(); + if (section.region != this) { + throw new IllegalStateException("Region section must point to us!"); + } + if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) { + throw new IllegalStateException("Region section must match the regionmanager state!"); + } + } + } + */ + + protected void mergeInto(final Region mergeTarget) { + if (this == mergeTarget) { + throw new IllegalStateException("Cannot merge a region onto itself"); + } + if (this.dead) { + throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget); + } else if (mergeTarget.dead) { + throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); + } + this.dead = true; + if (this.markedForRecalc) { + this.regionManager.removeFromRecalcQueue(this); + } + + for (final Iterator> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection section = iterator.next(); + + if (!mergeTarget.sections.add(section)) { + throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget); + } + + section.region = mergeTarget; + } + + for (final RegionSection deadSection : this.deadSections) { + if (!this.sections.contains(deadSection)) { + throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); + } + mergeTarget.deadSections.add(deadSection); + } + //mergeTarget.check(); + } + + protected void markSectionAlive(final RegionSection section) { + this.deadSections.remove(section); + if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) { + this.regionManager.removeFromRecalcQueue(this); + this.markedForRecalc = false; + } + } + + protected void markSectionDead(final RegionSection section) { + this.deadSections.add(section); + if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) { + this.regionManager.addToRecalcQueue(this); + this.markedForRecalc = true; + } + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(128); + + ret.append("Region{"); + ret.append("dead=").append(this.dead).append(','); + ret.append("markedForRecalc=").append(this.markedForRecalc).append(','); + + ret.append("sectionCount=").append(this.sections.size()).append(','); + ret.append("sections=["); + for (final Iterator> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final RegionSection section = iterator.next(); + ret.append(section); + if (iterator.hasNext()) { + ret.append(','); + } + } + ret.append(']'); + + ret.append('}'); + return ret.toString(); + } + } + + public static final class RegionSection & SingleThreadChunkRegionManager.RegionDataCreator> { + protected final long regionCoordinate; + protected final long[] chunksBitset = new long[Math.max(1, REGION_CHUNK_SIZE * REGION_CHUNK_SIZE / Long.SIZE)]; + protected int chunkCount; + protected Region region; + protected final EnumMap data; + protected final Function createIfAbsentFunction; + + public final SingleThreadChunkRegionManager regionManager; + + protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) { + this.regionCoordinate = regionCoordinate; + this.data = new EnumMap<>(regionManager.dataClass); + this.regionManager = regionManager; + this.createIfAbsentFunction = (final T keyInMap) -> { + return keyInMap.createData(RegionSection.this, regionManager); + }; + } + + public int getSectionX() { + return MCUtil.getCoordinateX(this.regionCoordinate); + } + + public int getSectionZ() { + return MCUtil.getCoordinateZ(this.regionCoordinate); + } + + public Region getRegion() { + return this.region; + } + + public Object getData(final T key) { + return this.data.get(key); + } + + public Object getOrCreateData(final T key) { + return this.data.computeIfAbsent(key, this.createIfAbsentFunction); + } + + public Object removeData(final T key) { + return this.data.remove(key); + } + + public void setData(final T key, final Object data) { + this.data.put(key, data); + } + + private static int getChunkIndex(final int chunkX, final int chunkZ) { + return (chunkX & (REGION_CHUNK_SIZE - 1)) | ((chunkZ & (REGION_CHUNK_SIZE - 1)) << REGION_CHUNK_SIZE_SHIFT); + } + + protected boolean hasChunks() { + return this.chunkCount != 0; + } + + protected void addChunk(final int chunkX, final int chunkZ) { + final int index = getChunkIndex(chunkX, chunkZ); + final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE + final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); + if (after == bitset) { + throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkCoordIntPair(chunkX, chunkZ).toString()); + } + if (++this.chunkCount != 1) { + return; + } + this.region.markSectionAlive(this); + } + + protected void removeChunk(final int chunkX, final int chunkZ) { + final int index = getChunkIndex(chunkX, chunkZ); + final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE + final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); + if (before == bitset) { + throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkCoordIntPair(chunkX, chunkZ).toString()); + } + if (--this.chunkCount != 0) { + return; + } + this.region.markSectionDead(this); + } + + @Override + public String toString() { + return "RegionSection{" + + "regionCoordinate=" + new ChunkCoordIntPair(this.regionCoordinate).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "hash=" + this.hashCode() + + "}"; + } + + public String toStringWithRegion() { + return "RegionSection{" + + "regionCoordinate=" + new ChunkCoordIntPair(this.regionCoordinate).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "hash=" + this.hashCode() + "," + + "region=" + this.region + + "}"; + } + + private static String toString(final long[] array) { + StringBuilder ret = new StringBuilder(); + for (long value : array) { + // zero pad the hex string + char[] zeros = new char[Long.SIZE / 4]; + Arrays.fill(zeros, '0'); + String string = Long.toHexString(value); + System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); + + ret.append(zeros); + } + + return ret.toString(); + } + } + + public static interface RegionDataCreator & RegionDataCreator> { + + Object createData(final RegionSection section, + final SingleThreadChunkRegionManager regionManager); + } +} \ No newline at end of file diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..eb330a40d7345336ded670d631a9fd66da19f2e7 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java @@ -0,0 +1,263 @@ +package com.tuinity.tuinity.chunk.light; + +import net.minecraft.server.BlockPosition; +import net.minecraft.server.Chunk; +import net.minecraft.server.ChunkSection; +import net.minecraft.server.ChunkStatus; +import net.minecraft.server.DataPaletteBlock; +import net.minecraft.server.IBlockData; +import net.minecraft.server.IChunkAccess; +import net.minecraft.server.ILightAccess; +import net.minecraft.server.ProtoChunkExtension; +import net.minecraft.server.VoxelShape; +import net.minecraft.server.VoxelShapes; +import net.minecraft.server.World; +import net.minecraft.server.WorldServer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public final class BlockStarLightEngine extends StarLightEngine { + + public BlockStarLightEngine(final World world) { + super(false, world); + } + + @Override + protected boolean[] getEmptinessMap(final IChunkAccess chunk) { + return null; + } + + @Override + protected void setEmptinessMap(final IChunkAccess chunk, final boolean[] to) {} + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk) { + return chunk.getBlockNibbles(); + } + + @Override + protected void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to) { + chunk.setBlockNibbles(to); + } + + @Override + protected boolean canUseChunk(final IChunkAccess chunk) { + return chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLit()); + } + + @Override + protected boolean[] handleEmptySectionChanges(final ILightAccess lightAccess, final IChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + return null; + } + + @Override + protected final void checkBlock(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change emitted light + // blocks can change direction of propagation + + final int encodeOffset = this.coordinateOffset; + final int emittedMask = this.emittedLightMask; + + final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers; + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + final IBlockData blockState = this.getBlockState(worldX, worldY, worldZ); + final int emittedLevel = (customBlockHandler != null ? this.getCustomLightLevel(customBlockHandler, worldX, worldY, worldZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask; + + this.setLightLevel(worldX, worldY, worldZ, emittedLevel); + // this accounts for change in emitted light that would cause an increase + if (emittedLevel != 0) { + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + } + // this also accounts for a change in emitted light that would cause a decrease + // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) + // as it checks all neighbours (even if current level is 0) + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + // always keep sided transparent false here, new block might be conditionally transparent which would + // prevent us from decreasing sources in the directions where the new block is opaque + // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always + // catch that and fix it. + ); + // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block + } + + protected final BlockPosition.MutableBlockPosition recalcCenterPos = new BlockPosition.MutableBlockPosition(); + protected final BlockPosition.MutableBlockPosition recalcNeighbourPos = new BlockPosition.MutableBlockPosition(); + + @Override + protected int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect, final VariableBlockLightHandler customBlockLight) { + final IBlockData centerState = this.getBlockState(worldX, worldY, worldZ); + int level = centerState.getEmittedLight() & 0xFF; + if (customBlockLight != null) { + level = this.getCustomLightLevel(customBlockLight, worldX, worldY, worldZ, level); + } + + if (level >= (15 - 1) || level > expect) { + return level; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final IBlockData conditionallyOpaqueState; + int opacity = centerState.getOpacityIfCached(); + + if (opacity == -1) { + this.recalcCenterPos.setValues(worldX, worldY, worldZ); + opacity = centerState.getOpacity(lightAccess.getWorld(), this.recalcCenterPos); + if (centerState.isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else if (opacity >= 15) { + return level; + } else { + conditionallyOpaqueState = null; + } + opacity = Math.max(1, opacity); + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final IBlockData neighbourState = this.getBlockState(offX, offY ,offZ); + + if (neighbourState.isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.setValues(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getCullingFace(lightAccess.getWorld(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? VoxelShapes.empty() : conditionallyOpaqueState.getCullingFace(lightAccess.getWorld(), this.recalcCenterPos, direction.nms); + if (VoxelShapes.combinationOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + // passed transparency, + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final ILightAccess lightAccess, final IChunkAccess atChunk, final Set positions) { + for (final BlockPosition pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + protected Iterator getSources(final ILightAccess lightAccess, final IChunkAccess chunk) { + if (chunk instanceof ProtoChunkExtension || chunk instanceof Chunk) { + // implementation on Chunk is pretty awful, so write our own here. The big optimisation is + // skipping empty sections, and the far more optimised reading of types. + List sources = new ArrayList<>(); + + int offX = chunk.getPos().x << 4; + int offZ = chunk.getPos().z << 4; + + final ChunkSection[] sections = chunk.getSections(); + for (int sectionY = 0; sectionY <= 15; ++sectionY) { + if (sections[sectionY] == null || sections[sectionY].isFullOfAir()) { + // no sources in empty sections + continue; + } + final DataPaletteBlock section = sections[sectionY].blockIds; + final int offY = sectionY << 4; + + for (int index = 0; index < (16 * 16 * 16); ++index) { + final IBlockData state = section.rawGet(index); + if (state.getEmittedLight() <= 0) { + continue; + } + + // index = x | (z << 4) | (y << 8) + sources.add(new BlockPosition(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); + } + } + + final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers; + if (customBlockHandler == null) { + return sources.iterator(); + } + + final Set ret = new HashSet<>(sources); + ret.addAll(customBlockHandler.getCustomLightPositions(chunk.getPos().x, chunk.getPos().z)); + + return ret.iterator(); + } else { + return chunk.getLightSources().iterator(); + } + } + + @Override + public void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks) { + // setup sources + final int emittedMask = this.emittedLightMask; + final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers; + for (final Iterator positions = this.getSources(lightAccess, chunk); positions.hasNext();) { + final BlockPosition pos = positions.next(); + final IBlockData blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); + final int emittedLight = (customBlockHandler != null ? this.getCustomLightLevel(customBlockHandler, pos.getX(), pos.getY(), pos.getZ(), blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask; + + if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { + // some other source is brighter + continue; + } + + this.appendToIncreaseQueue( + ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLight & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + + + // propagation wont set this for us + this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); + } + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + // verify neighbour edges + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + } else { + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); + + this.performLightIncrease(lightAccess); + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java b/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java new file mode 100644 index 0000000000000000000000000000000000000000..051e2db5349b6f20887841efad7fbc183b190f68 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java @@ -0,0 +1,325 @@ +package com.tuinity.tuinity.chunk.light; + +import net.minecraft.server.NibbleArray; +import java.util.ArrayDeque; +import java.util.Arrays; + +// SWMR -> Single Writer Multi Reader Nibble Array +public final class SWMRNibbleArray { + + /* + * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null + * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised + * nibbles can be written to. + * + * Uninitialised nibble - They are all 0, but the backing array isn't initialised. + * + * Initialised nibble - Has light data. + */ + + protected static final int INIT_STATE_NULL = 0; // null + protected static final int INIT_STATE_UNINIT = 1; // uninitialised + protected static final int INIT_STATE_INIT = 2; // initialised + + public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block + protected static final byte[] FULL_LIT = new byte[ARRAY_SIZE]; + static { + Arrays.fill(FULL_LIT, (byte)-1); + } + // this allows us to maintain only 1 byte array when we're not updating + static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); + + private static byte[] allocateBytes() { + final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); + if (inPool != null) { + return inPool; + } + + return new byte[ARRAY_SIZE]; + } + + private static void freeBytes(final byte[] bytes) { + WORKING_BYTES_POOL.get().addFirst(bytes); + } + + protected int stateUpdating; + protected volatile int stateVisible; + + protected byte[] storageUpdating; + protected boolean updatingDirty; // only returns whether storageUpdating is dirty + protected byte[] storageVisible; + + public SWMRNibbleArray() { + this(null, false); // lazy init + } + + public SWMRNibbleArray(final byte[] bytes) { + this(bytes, false); + } + + public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { + if (bytes != null && bytes.length != ARRAY_SIZE) { + throw new IllegalArgumentException(); + } + this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; + this.storageUpdating = this.storageVisible = bytes; + } + + // operation type: visible + public boolean isAllZero() { + final int state = this.stateVisible; + + if (state == INIT_STATE_NULL) { + return false; + } else if (state == INIT_STATE_UNINIT) { + return true; + } + + synchronized (this) { + final byte[] bytes = this.storageVisible; + + if (bytes == null) { + return this.stateVisible == INIT_STATE_UNINIT; + } + + for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { + byte whole = bytes[i << 4]; + + for (int k = 1; k < (1 << 4); ++k) { + whole |= bytes[(i << 4) | k]; + } + + if (whole != 0) { + return false; + } + } + } + + return true; + } + + // operation type: updating on src, updating on other + public void extrudeLower(final SWMRNibbleArray other) { + if (other.stateUpdating == INIT_STATE_NULL) { + throw new IllegalArgumentException(); + } + + if (other.storageUpdating == null) { + this.setUninitialised(); + return; + } + + final byte[] src = other.storageUpdating; + final byte[] into; + + if (this.storageUpdating != null) { + into = this.storageUpdating; + } else { + this.storageUpdating = into = allocateBytes(); + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + + final int start = 0; + final int end = (15 | (15 << 4)) >>> 1; + + /* x | (z << 4) | (y << 8) */ + for (int y = 0; y <= 15; ++y) { + System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); + } + } + + // operation type: updating + public void setFull() { + this.stateUpdating = INIT_STATE_INIT; + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); + this.updatingDirty = true; + } + + // operation type: updating + public void setZero() { + this.stateUpdating = INIT_STATE_INIT; + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); + this.updatingDirty = true; + } + + // operation type: updating + public void setNonNull() { + if (this.stateUpdating != INIT_STATE_NULL) { + return; + } + this.stateUpdating = INIT_STATE_UNINIT; + } + + // operation type: updating + public void setNull() { + this.stateUpdating = INIT_STATE_NULL; + if (this.updatingDirty && this.storageUpdating != null) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setUninitialised() { + this.stateUpdating = INIT_STATE_UNINIT; + if (this.storageUpdating != null && this.updatingDirty) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public boolean isDirty() { + return this.stateUpdating != this.stateVisible || this.updatingDirty; + } + + // operation type: updating + public boolean isNullNibbleUpdating() { + return this.stateUpdating == INIT_STATE_NULL; + } + + // operation type: visible + public boolean isNullNibbleVisible() { + return this.stateVisible == INIT_STATE_NULL; + } + + // opeartion type: updating + public boolean isUninitialisedUpdating() { + return this.stateUpdating == INIT_STATE_UNINIT; + } + + // operation type: visible + public boolean isUninitialisedVisible() { + return this.stateVisible == INIT_STATE_UNINIT; + } + + // operation type: updating + public boolean isInitialisedUpdating() { + return this.stateUpdating == INIT_STATE_INIT; + } + + // operation type: visible + public boolean isInitialisedVisible() { + return this.stateVisible == INIT_STATE_INIT; + } + + // operation type: updating + protected void swapUpdatingAndMarkDirty() { + if (this.updatingDirty) { + return; + } + + if (this.storageUpdating == null) { + this.storageUpdating = allocateBytes(); + Arrays.fill(this.storageUpdating, (byte)0); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); + } + + this.stateUpdating = INIT_STATE_INIT; + this.updatingDirty = true; + } + + // operation type: updating + public boolean updateVisible() { + if (!this.isDirty()) { + return false; + } + + synchronized (this) { + if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { + this.storageVisible = null; + } else { + if (this.storageVisible == null) { + this.storageVisible = this.storageUpdating.clone(); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); + } + + freeBytes(this.storageUpdating); + this.storageUpdating = this.storageVisible; + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; + } + + return true; + } + + // operation type: visible + public NibbleArray toVanillaNibble() { + synchronized (this) { + switch (this.stateVisible) { + case INIT_STATE_NULL: + return null; + case INIT_STATE_UNINIT: + return new NibbleArray(); + case INIT_STATE_INIT: + return new NibbleArray(this.storageVisible.clone()); + default: + throw new IllegalStateException(); + } + } + } + + /* x | (z << 4) | (y << 8) */ + + // operation type: updating + public int getUpdating(final int x, final int y, final int z) { + return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: updating + public int getUpdating(final int index) { + // indices range from 0 -> 4096 + final byte[] bytes = this.storageUpdating; + if (bytes == null) { + return 0; + } + final byte value = bytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + + // operation type: visible + public int getVisible(final int x, final int y, final int z) { + return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); + } + + // operation type: visible + public int getVisible(final int index) { + synchronized (this) { + // indices range from 0 -> 4096 + final byte[] visibleBytes = this.storageVisible; + if (visibleBytes == null) { + return 0; + } + final byte value = visibleBytes[index >>> 1]; + + // if we are an even index, we want lower 4 bits + // if we are an odd index, we want upper 4 bits + return ((value >>> ((index & 1) << 2)) & 0xF); + } + } + + // operation type: updating + public void set(final int x, final int y, final int z, final int value) { + this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); + } + + // operation type: updating + public void set(final int index, final int value) { + if (!this.updatingDirty) { + this.swapUpdatingAndMarkDirty(); + } + final int shift = (index & 1) << 2; + final int i = index >>> 1; + + this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..64c68f3be9f9f97ec3f9fd5c7fd0221ceb333b1a --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java @@ -0,0 +1,828 @@ +package com.tuinity.tuinity.chunk.light; + +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.server.BlockPosition; +import net.minecraft.server.ChunkCoordIntPair; +import net.minecraft.server.ChunkSection; +import net.minecraft.server.ChunkStatus; +import net.minecraft.server.IBlockAccess; +import net.minecraft.server.IBlockData; +import net.minecraft.server.IChunkAccess; +import net.minecraft.server.ILightAccess; +import net.minecraft.server.VoxelShape; +import net.minecraft.server.VoxelShapes; +import net.minecraft.server.World; +import java.util.Arrays; +import java.util.Set; + +public final class SkyStarLightEngine extends StarLightEngine { + + /* + Specification for managing the initialisation and de-initialisation of skylight nibble arrays: + + Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null. + + This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks. + However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees + that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise + our own) - we need a radius of 2 to de-initialise neighbour nibbles. + How do we solve this? + + Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections. + If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the + chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last + known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data + to see if any of its nibbles need to be de-initialised. + + The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data, + and if it doesn't have data then we know it will correctly de-initialise once it fills up. + + Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking + around those. + */ + + protected final int[] heightMapBlockChange = new int[16 * 16]; + { + Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap + } + + protected final boolean[] nullPropagationCheckCache; + + public SkyStarLightEngine(final World world) { + super(true, world); + this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)]; + } + + @Override + protected boolean[] handleEmptySectionChanges(final ILightAccess lightAccess, final IChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + final World world = (World)lightAccess.getWorld(); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + + boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ); + boolean[] ret = null; + final boolean needsInit = unlit || chunkEmptinessMap == null; + if (needsInit) { + this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]); + } + + // update emptiness map + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + if (valueBoxed == null) { + if (needsInit) { + throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?"); + } + continue; + } + chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue(); + } + + // now init neighbour nibbles + for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) { + final Boolean valueBoxed = emptinessChanges[sectionIndex]; + final int sectionY = sectionIndex + this.minSection; + if (valueBoxed == null) { + continue; + } + + final boolean empty = valueBoxed.booleanValue(); + + if (empty) { + continue; + } + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // if we're not empty, we also need to initialise nibbles + // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up + final boolean extrude = (dx | dz) != 0 || !unlit; + for (int dy = 1; dy >= -1; --dy) { + this.initNibbleForLitChunk(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + // check for de-init and lazy-init + // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running + // init checks. + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + // does this neighbour have 1 radius loaded? + boolean neighboursLoaded = true; + neighbour_loaded_search: + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) { + neighboursLoaded = false; + break neighbour_loaded_search; + } + } + } + + for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, sectionY, dz + chunkZ); + + // check neighbours to see if we need to de-init this one + boolean allEmpty = true; + neighbour_search: + for (int dy2 = -1; dy2 <= 1; ++dy2) { + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int y = sectionY + dy2; + if (y < this.minSection || y > this.maxSection) { + // empty + continue; + } + final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ); + if (emptinessMap != null) { + if (!emptinessMap[y - this.minSection]) { + allEmpty = false; + break neighbour_search; + } + } else { + final ChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ); + if (section != null && section != EMPTY_CHUNK_SECTION) { + allEmpty = false; + break neighbour_search; + } + } + } + } + } + + if (allEmpty & neighboursLoaded) { + // can only de-init when neighbours are loaded + // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting + // to be correct + + // all were empty, so de-init + if (nibble != null) { + nibble.setNull(); + } + } else if (!allEmpty) { + // must init + final boolean extrude = (dx | dz) != 0 || !unlit; + this.initNibbleForLitChunk(dx + chunkX, sectionY, dz + chunkZ, extrude, false); + } + } + } + } + + return ret; + } + + + protected final void initNibbleForLitChunk(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, + final boolean initRemovedNibbles) { + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { + return; + } + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble == null) { + if (!initRemovedNibbles) { + throw new IllegalStateException(); + } else { + this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true)); + } + } + this.initNibbleForLitChunk(nibble, chunkX, chunkY, chunkZ, extrude); + } + + protected final void initNibbleForLitChunk(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) { + if (!currNibble.isNullNibbleUpdating()) { + // already initialised + return; + } + + final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ); + + // are we above this chunk's lowest empty section? + int lowestY = this.minLightSection - 1; + for (int currY = this.maxSection; currY >= this.minSection; --currY) { + if (emptinessMap == null) { + // cannot delay nibble init for lit chunks, as we need to init to propagate into them. + final ChunkSection current = this.getChunkSection(chunkX, currY, chunkZ); + if (current == null || current == EMPTY_CHUNK_SECTION) { + continue; + } + } else { + if (emptinessMap[currY - this.minSection]) { + continue; + } + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (chunkY > lowestY) { + // we need to set this one to full + this.getNibbleFromCache(chunkX, chunkY, chunkZ).setFull(); + return; + } + + if (extrude) { + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + currNibble.extrudeLower(nibble); + break; + } + } + } else { + currNibble.setNonNull(); + } + } + + protected final void rewriteNibbleCacheForSkylight(final IChunkAccess chunk) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (nibble != null && nibble.isNullNibbleUpdating()) { + // stop propagation in these areas + this.nibbleCache[index] = null; + nibble.updateVisible(); + } + } + } + + // rets whether neighbours were init'd + + protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ, + final boolean extrudeInitialised) { + // null chunk sections may have nibble neighbours in the horizontal 1 radius that are + // non-null. Propagation to these neighbours is necessary. + // What makes this easy is we know none of these neighbours are non-empty (otherwise + // this nibble would be initialised). So, we don't have to initialise + // the neighbours in the full 1 radius, because there's no worry that any "paths" + // to the neighbours on this horizontal plane are blocked. + if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) { + return false; + } + this.nullPropagationCheckCache[chunkY - this.minLightSection] = true; + + // check horizontal neighbours + boolean needInitNeighbours = false; + neighbour_search: + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ); + if (nibble != null && !nibble.isNullNibbleUpdating()) { + needInitNeighbours = true; + break neighbour_search; + } + } + } + + if (needInitNeighbours) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + this.initNibbleForLitChunk(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true); + } + } + } + + return needInitNeighbours; + } + + protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) { + final int chunkX = worldX >> 4; + int chunkY = worldY >> 4; + final int chunkZ = worldZ >> 4; + + SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (nibble != null) { + return nibble.getUpdating(worldX, worldY, worldZ); + } + + for (;;) { + if (++chunkY > this.maxLightSection) { + return 15; + } + + nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + + if (nibble != null) { + return nibble.getUpdating(worldX, 0, worldZ); + } + } + } + + @Override + protected boolean[] getEmptinessMap(final IChunkAccess chunk) { + return chunk.getEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final IChunkAccess chunk, final boolean[] to) { + chunk.setEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk) { + return chunk.getSkyNibbles(); + } + + @Override + protected void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to) { + chunk.setSkyNibbles(to); + } + + @Override + protected boolean canUseChunk(final IChunkAccess chunk) { + // can only use chunks for sky stuff if their sections have been init'd + return chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT) && (this.isClientSide ? true : chunk.isLit()); + } + + @Override + protected void checkChunkEdges(final ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, + final int toSection) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (int y = toSection; y >= fromSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); + } + + @Override + protected void checkChunkEdges(final ILightAccess lightAccess, final IChunkAccess chunk, final ShortCollection sections) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + final int y = (int)iterator.nextShort(); + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, sections); + } + + + protected final BlockPosition.MutableBlockPosition recalcCenterPos = new BlockPosition.MutableBlockPosition(); + protected final BlockPosition.MutableBlockPosition recalcNeighbourPos = new BlockPosition.MutableBlockPosition(); + + @Override + protected int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect, final VariableBlockLightHandler customBlockLight) { + if (expect == 15) { + return expect; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final IBlockData centerState = this.getBlockState(worldX, worldY, worldZ); + int opacity = centerState.getOpacityIfCached(); + + + final IBlockData conditionallyOpaqueState; + if (opacity < 0) { + this.recalcCenterPos.setValues(worldX, worldY, worldZ); + opacity = Math.max(1, centerState.getOpacity(lightAccess.getWorld(), this.recalcCenterPos)); + if (centerState.isConditionallyFullOpaque()) { + conditionallyOpaqueState = centerState; + } else { + conditionallyOpaqueState = null; + } + } else { + conditionallyOpaqueState = null; + opacity = Math.max(1, opacity); + } + + int level = 0; + + for (final AxisDirection direction : AXIS_DIRECTIONS) { + final int offX = worldX + direction.x; + final int offY = worldY + direction.y; + final int offZ = worldZ + direction.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + + final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); + + if ((neighbourLevel - 1) <= level) { + // don't need to test transparency, we know it wont affect the result. + continue; + } + + final IBlockData neighbourState = this.getBlockState(offX, offY ,offZ); + + if (neighbourState.isConditionallyFullOpaque()) { + // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that + // we don't read the blockstate because most of the time this is false, so using the faster + // known transparency lookup results in a net win + this.recalcNeighbourPos.setValues(offX, offY, offZ); + final VoxelShape neighbourFace = neighbourState.getCullingFace(lightAccess.getWorld(), this.recalcNeighbourPos, direction.opposite.nms); + final VoxelShape thisFace = conditionallyOpaqueState == null ? VoxelShapes.empty() : conditionallyOpaqueState.getCullingFace(lightAccess.getWorld(), this.recalcCenterPos, direction.nms); + if (VoxelShapes.combinationOccludes(thisFace, neighbourFace)) { + // not allowed to propagate + continue; + } + } + + // passed transparency, + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void checkBlock(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ) { + // blocks can change opacity + // blocks can change direction of propagation + + // same logic applies from BlockStarLightEngine#checkBlock + + final int encodeOffset = this.coordinateOffset; + + final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); + + if (currentLevel == 15) { + // must re-propagate clobbered source + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent + ); + } else { + this.setLightLevel(worldX, worldY, worldZ, 0); + } + + this.appendToDecreaseQueue( + ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (currentLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + ); + } + + @Override + protected void propagateBlockChanges(final ILightAccess lightAccess, final IChunkAccess atChunk, final Set positions) { + this.rewriteNibbleCacheForSkylight(atChunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final IBlockAccess world = lightAccess.getWorld(); + final int chunkX = atChunk.getPos().x; + final int chunkZ = atChunk.getPos().z; + final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16)); + + // setup heightmap for changes + for (final BlockPosition pos : positions) { + final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset; + final int curr = this.heightMapBlockChange[index]; + if (pos.getY() > curr) { + this.heightMapBlockChange[index] = pos.getY(); + } + } + + // note: light sets are delayed while processing skylight source changes due to how + // nibbles are initialised, as we want to avoid clobbering nibble values so what when + // below nibbles are initialised they aren't reading from partially modified nibbles + + // now we can recalculate the sources for the changed columns + for (int index = 0; index < (16 * 16); ++index) { + final int maxY = this.heightMapBlockChange[index]; + if (maxY == Integer.MIN_VALUE) { + // not changed + continue; + } + this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller + + final int columnX = (index & 15) | (chunkX << 4); + final int columnZ = (index >>> 4) | (chunkZ << 4); + + // try and propagate from the above y + // delay light set until after processing all sources to setup + final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true); + + // maxPropagationY is now the highest block that could not be propagated to + + // remove all sources below that are 15 + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; + final int encodeOffset = this.coordinateOffset; + + if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true); + + for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) { + if ((currY & 15) == 15) { + // ensure section is checked + this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true); + } + + // ensure section below is always checked + final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4); + if (nibble == null) { + // advance currY to the the top of the section below + currY = (currY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + continue; + } + + if (nibble.getUpdating(columnX, currY, columnZ) != 15) { + break; + } + + // delay light set until after processing all sources to setup + this.appendToDecreaseQueue( + ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + // do not set transparent blocks for the same reason we don't in the checkBlock method + ); + } + } + } + + // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads + // immediate light value + this.processDelayedIncreases(); + this.processDelayedDecreases(); + + for (final BlockPosition pos : positions) { + this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); + } + + this.performLightDecrease(lightAccess); + } + + @Override + protected void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks) { + this.rewriteNibbleCacheForSkylight(chunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final IBlockAccess world = lightAccess.getWorld(); + final ChunkCoordIntPair chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + final ChunkSection[] sections = chunk.getSections(); + + int highestNonEmptySection = this.maxSection; + while (highestNonEmptySection == (this.minSection - 1) || + sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].isFullOfAir()) { + this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false); + // try propagate FULL to neighbours + + // check neighbours to see if we need to propagate into them + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourX = chunkX + direction.x; + final int neighbourZ = chunkZ + direction.z; + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ); + if (neighbourNibble == null) { + // unloaded neighbour + // most of the time we fall here + continue; + } + + // it looks like we need to propagate into the neighbour + + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (direction.x != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (direction.z < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction + + for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY) + ); + } + } + } + + if (highestNonEmptySection-- == (this.minSection - 1)) { + break; + } + } + + if (highestNonEmptySection >= this.minSection) { + // fill out our other sources + final int minX = chunkPos.x << 4; + final int maxX = chunkPos.x << 4 | 15; + final int minZ = chunkPos.z << 4; + final int maxZ = chunkPos.z << 4 | 15; + final int startY = highestNonEmptySection << 4 | 15; + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false); + } + } + } // else: apparently the chunk is empty + + if (needsEdgeChecks) { + // not required to propagate here, but this will reduce the hit of the edge checks + this.performLightIncrease(lightAccess); + + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + // no need to rewrite the nibble cache again + super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + } else { + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection); + + this.performLightIncrease(lightAccess); + } + } + + protected final void processDelayedIncreases() { + // copied from performLightIncrease + final long[] queue = this.increaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + + this.setLightLevel(posX, posY, posZ, propagatedLightLevel); + } + } + + protected final void processDelayedDecreases() { + // copied from performLightDecrease + final long[] queue = this.decreaseQueue; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + + for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) { + final long queueValue = queue[i]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + + this.setLightLevel(posX, posY, posZ, 0); + } + } + + // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays + // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so + // clobbering the light values will result in broken propagation) + protected final int tryPropagateSkylight(final IBlockAccess world, final int worldX, int startY, final int worldZ, + final boolean extrudeInitialised, final boolean delayLightSet) { + final BlockPosition.MutableBlockPosition mutablePos = this.mutablePos3; + final int encodeOffset = this.coordinateOffset; + final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards. + + if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) { + return startY; + } + + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + + IBlockData above = this.getBlockState(worldX, startY + 1, worldZ); + if (above == null) { + above = AIR_BLOCK_STATE; + } + + for (;startY >= (this.minLightSection << 4); --startY) { + if ((startY & 15) == 15) { + // ensure this section is always checked + this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised); + } + IBlockData current = this.getBlockState(worldX, startY, worldZ); + if (current == null) { + current = AIR_BLOCK_STATE; + } + + final VoxelShape fromShape; + if (above.isConditionallyFullOpaque()) { + this.mutablePos2.setValues(worldX, startY + 1, worldZ); + fromShape = above.getCullingFace(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms); + if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), fromShape)) { + // above wont let us propagate + break; + } + } else { + fromShape = VoxelShapes.getEmptyShape(); + } + + final int opacityIfCached = current.getOpacityIfCached(); + // does light propagate from the top down? + if (opacityIfCached != -1) { + if (opacityIfCached != 0) { + // we cannot propagate 15 through this + break; + } + // most of the time it falls here. + // add to propagate + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + ); + } else { + mutablePos.setValues(worldX, startY, worldZ); + long flags = 0L; + if (current.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = current.getCullingFace(world, mutablePos, AxisDirection.POSITIVE_Y.nms); + + if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) { + // can't propagate here, we're done on this column. + break; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = current.getOpacity(world, mutablePos); + if (opacity > 0) { + // let the queued value (if any) handle it from here. + break; + } + + // light set delayed until we determine if this nibble section is null + this.appendToIncreaseQueue( + ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (15L << (6 + 6 + 16)) // we know we're at full lit here + | (propagateDirection << (6 + 6 + 16 + 4)) + | flags + ); + } + + above = current; + + if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) { + // we skip empty sections here, as this is just an easy way of making sure the above block + // can propagate through air. + + // nothing can propagate in null sections, remove the queue entry for it + --this.increaseQueueInitialLength; + + // advance currY to the the top of the section below + startY = (startY) & (~15); + // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually + // end up there + + // make sure this is marked as AIR + above = AIR_BLOCK_STATE; + } else if (!delayLightSet) { + this.setLightLevel(worldX, startY, worldZ, 15); + } + } + + return startY; + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..f20fd2126a21b9c7c45fc420b67c645af875f929 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java @@ -0,0 +1,1491 @@ +package com.tuinity.tuinity.chunk.light; + +import com.tuinity.tuinity.util.CoordinateUtils; +import com.tuinity.tuinity.util.IntegerUtil; +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minecraft.server.BlockPosition; +import net.minecraft.server.Blocks; +import net.minecraft.server.ChunkCoordIntPair; +import net.minecraft.server.ChunkSection; +import net.minecraft.server.EnumDirection; +import net.minecraft.server.EnumSkyBlock; +import net.minecraft.server.IBlockAccess; +import net.minecraft.server.IBlockData; +import net.minecraft.server.IChunkAccess; +import net.minecraft.server.ILightAccess; +import net.minecraft.server.SectionPosition; +import net.minecraft.server.VoxelShape; +import net.minecraft.server.VoxelShapes; +import net.minecraft.server.World; +import net.minecraft.server.WorldServer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StarLightEngine { + + protected static final IBlockData AIR_BLOCK_STATE = Blocks.AIR.getBlockData(); + + protected static final ChunkSection EMPTY_CHUNK_SECTION = new ChunkSection(0); + + protected static final AxisDirection[] DIRECTIONS = AxisDirection.values(); + protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS; + protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] { + AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X, + AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z + }; + + protected static enum AxisDirection { + + // Declaration order is important and relied upon. Do not change without modifying propagation code. + POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0), + POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1), + POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0); + + static { + POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X; + POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z; + POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y; + } + + protected AxisDirection opposite; + + public final int x; + public final int y; + public final int z; + public final EnumDirection nms; + public final long everythingButThisDirection; + public final long everythingButTheOppositeDirection; + + AxisDirection(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + this.nms = EnumDirection.from(x, y, z); + this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal())); + // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction. + this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1))); + } + + public AxisDirection getOpposite() { + return this.opposite; + } + } + + // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1 + // for explaining how light propagates via breadth-first search + + // While the above is a good start to understanding the general idea of what the general principles are, it's not + // exactly how the vanilla light engine should behave for minecraft. + + // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2] + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + // null index indicates the chunk section doesn't exist (empty or out of bounds) + protected final ChunkSection[] sectionCache; + + // the exact same as above, except for storing fast access to SWMRNibbleArray + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final SWMRNibbleArray[] nibbleCache; + + // the exact same as above, except for storing fast access to nibbles to call change callbacks for + // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection] + // index = x + (z * 5) + (y * 25) + protected final boolean[] notifyUpdateCache; + + // always initialsed during start of lighting. no index is null. + // index = x + (z * 5) + protected final IChunkAccess[] chunkCache = new IChunkAccess[5 * 5]; + + // index = x + (z * 5) + protected final boolean[][] emptinessMapCache = new boolean[5 * 5][]; + + protected final BlockPosition.MutableBlockPosition mutablePos1 = new BlockPosition.MutableBlockPosition(); + protected final BlockPosition.MutableBlockPosition mutablePos2 = new BlockPosition.MutableBlockPosition(); + protected final BlockPosition.MutableBlockPosition mutablePos3 = new BlockPosition.MutableBlockPosition(); + + protected int encodeOffsetX; + protected int encodeOffsetY; + protected int encodeOffsetZ; + + protected int coordinateOffset; + + protected int chunkOffsetX; + protected int chunkOffsetY; + protected int chunkOffsetZ; + + protected int chunkIndexOffset; + protected int chunkSectionIndexOffset; + + protected final boolean skylightPropagator; + protected final int emittedLightMask; + protected static final boolean isClientSide = false; + + protected final World world; + protected final int minLightSection; + protected final int maxLightSection; + protected final int minSection; + protected final int maxSection; + + protected StarLightEngine(final boolean skylightPropagator, final World world) { + this.skylightPropagator = skylightPropagator; + this.emittedLightMask = skylightPropagator ? 0 : 0xF; + //this.isClientSide = isClientSide; + this.world = world; + this.minLightSection = WorldUtil.getMinLightSection(world); + this.maxLightSection = WorldUtil.getMaxLightSection(world); + this.minSection = WorldUtil.getMinSection(world); + this.maxSection = WorldUtil.getMaxSection(world); + + this.sectionCache = new ChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer + } + + protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) { + // 31 = center + encodeOffset + this.encodeOffsetX = 31 - centerX; + this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value + this.encodeOffsetZ = 31 - centerZ; + + // coordinateIndex = x | (z << 6) | (y << 12) + this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12); + + // 2 = (centerX >> 4) + chunkOffset + this.chunkOffsetX = 2 - (centerX >> 4); + this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0 + this.chunkOffsetZ = 2 - (centerZ >> 4); + + // chunk index = x + (5 * z) + this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ); + + // chunk section index = x + (5 * z) + ((5*5) * y) + this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY); + } + + protected final void setupCaches(final ILightAccess chunkProvider, final int centerX, final int centerY, final int centerZ, + final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { + final int centerChunkX = centerX >> 4; + final int centerChunkY = centerY >> 4; + final int centerChunkZ = centerZ >> 4; + + this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7); + + final int radius = tryToLoadChunksFor2Radius ? 2 : 1; + + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + final int cx = centerChunkX + dx; + final int cz = centerChunkZ + dz; + final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2; + final IChunkAccess chunk = (IChunkAccess)chunkProvider.getFeaturesReadyChunk(cx, cz); // mappings are awful here, this is the "get chunk at if at least features" + + if (chunk == null) { + if (relaxed | isTwoRadius) { + continue; + } + throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready"); + } + + if (!this.canUseChunk(chunk)) { + continue; + } + + this.setChunkInCache(cx, cz, chunk); + this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk)); + if (!isTwoRadius) { + this.setBlocksForChunkInCache(cx, cz, chunk.getSections()); + this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk)); + } + } + } + } + + protected final IChunkAccess getChunkInCache(final int chunkX, final int chunkZ) { + return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setChunkInCache(final int chunkX, final int chunkZ, final IChunkAccess chunk) { + this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; + } + + protected final ChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) { + return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final ChunkSection section) { + this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; + } + + protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final ChunkSection[] sections) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setChunkSectionInCache(chunkX, cy, chunkZ, + sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? (sections[cy - this.minSection] == null || sections[cy - this.minSection].isFullOfAir() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION)); + } + } + + protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) { + return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset]; + } + + protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1]; + + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset]; + } + + return ret; + } + + protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) { + this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble; + } + + protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) { + for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) { + this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]); + } + } + + protected final void updateVisible(final ILightAccess lightAccess) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) { + continue; + } + + final int chunkX = (index % 5) - this.chunkOffsetX; + final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ; + final int chunkY = ((index / (5*5)) % (16 + 2 + 2)) - this.chunkOffsetY; + if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) { + lightAccess.markLightSectionDirty(this.skylightPropagator ? EnumSkyBlock.SKY : EnumSkyBlock.BLOCK, new SectionPosition(chunkX, chunkY, chunkZ)); + } + } + } + + protected final void destroyCaches() { + Arrays.fill(this.sectionCache, null); + Arrays.fill(this.nibbleCache, null); + Arrays.fill(this.chunkCache, null); + Arrays.fill(this.emptinessMapCache, null); + if (this.isClientSide) { + Arrays.fill(this.notifyUpdateCache, false); + } + } + + protected final IBlockData getBlockState(final int worldX, final int worldY, final int worldZ) { + final ChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + if (section != null) { + return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getType(worldX & 15, worldY & 15, worldZ & 15); + } + + return null; + } + + protected final IBlockData getBlockState(final int sectionIndex, final int localIndex) { + final ChunkSection section = this.sectionCache[sectionIndex]; + + if (section != null) { + return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.blockIds.rawGet(localIndex); + } + + return null; + } + + protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) { + final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8)); + } + + protected final int getLightLevel(final int sectionIndex, final int localIndex) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + return nibble == null ? 0 : nibble.getUpdating(localIndex); + } + + protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) { + final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset; + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + + protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) { + final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex]; + + if (nibble != null) { + nibble.set(localIndex, level); + if (this.isClientSide) { + int cx1 = (worldX - 1) >> 4; + int cx2 = (worldX + 1) >> 4; + int cy1 = (worldY - 1) >> 4; + int cy2 = (worldY + 1) >> 4; + int cz1 = (worldZ - 1) >> 4; + int cz2 = (worldZ + 1) >> 4; + for (int x = cx1; x <= cx2; ++x) { + for (int y = cy1; y <= cy2; ++y) { + for (int z = cz1; z <= cz2; ++z) { + this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true; + } + } + } + } + } + } + + protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) { + return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset]; + } + + protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) { + this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap; + } + + protected final int getCustomLightLevel(final VariableBlockLightHandler customBlockHandler, final int worldX, final int worldY, + final int worldZ, final int dfl) { + final int ret = customBlockHandler.getLightLevel(worldX, worldY, worldZ); + return ret == -1 ? dfl : ret; + } + + // :( + + protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) { + throw new UnsupportedOperationException(); + } + + // warn: localIndex = y | (x << 4) | (z << 8) + protected final long getKnownTransparency(final int sectionIndex, final int localIndex) { + throw new UnsupportedOperationException(); + } + + /** + * @deprecated To be removed in 1.17 due to variable section count + */ + @Deprecated + public static SWMRNibbleArray[] getFilledEmptyLight() { + return getFilledEmptyLight(16 - (-1) + 1); + } + + public static SWMRNibbleArray[] getFilledEmptyLight(final World world) { + return getFilledEmptyLight(WorldUtil.getTotalLightSections(world)); + } + + private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) { + final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections]; + + for (int i = 0, len = ret.length; i < len; ++i) { + ret[i] = new SWMRNibbleArray(null, true); + } + + return ret; + } + + protected abstract boolean[] getEmptinessMap(final IChunkAccess chunk); + + protected abstract void setEmptinessMap(final IChunkAccess chunk, final boolean[] to); + + protected abstract SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk); + + protected abstract void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to); + + protected abstract boolean canUseChunk(final IChunkAccess chunk); + + public final void blocksChangedInChunk(final ILightAccess lightAccess, final int chunkX, final int chunkZ, + final Set positions, final Boolean[] changedSections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + final IChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (this.isClientSide && chunk == null) { + return; + } + if (changedSections != null) { + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + } + if (!positions.isEmpty()) { + this.propagateBlockChanges(lightAccess, chunk, positions); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + protected abstract void propagateBlockChanges(final ILightAccess lightAccess, final IChunkAccess atChunk, final Set positions); + + protected abstract void checkBlock(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ); + + // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual) + // if ret == expect, then expect is the correct light value for pos + // if ret < expect, then ret is the real light value + protected abstract int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect, final VariableBlockLightHandler customBlockLight); + + protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16]; + protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16]; + + protected void checkChunkEdge(final ILightAccess lightAccess, final IChunkAccess chunk, + final int chunkX, final int chunkY, final int chunkZ) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); + if (currNibble == null) { + return; + } + + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + chunkY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null) { + continue; + } + + if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) { + // both are zero, nothing to check. + continue; + } + + // this chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = chunkX << 4; + } else { + startX = chunkX << 4 | 15; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = chunkZ << 4; + } else { + startZ = chunkZ << 4 | 15; + } + startX = chunkX << 4; + } + + final VariableBlockLightHandler customLightHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers; + int centerDelayedChecks = 0; + int neighbourDelayedChecks = 0; + for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int neighbourX = currX + neighbourOffX; + final int neighbourZ = currZ + neighbourOffZ; + + final int currentIndex = (currX & 15) | + ((currZ & 15)) << 4 | + ((currY & 15) << 8); + final int currentLevel = currNibble.getUpdating(currentIndex); + + final int neighbourIndex = + (neighbourX & 15) | + ((neighbourZ & 15)) << 4 | + ((currY & 15) << 8); + final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex); + + // the checks are delayed because the checkBlock method clobbers light values - which then + // affect later calculate light value operations. While they don't affect it in a behaviourly significant + // way, they do have a negative performance impact due to simply queueing more values + + if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel, customLightHandler) != currentLevel) { + this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex; + } + + if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel, customLightHandler) != neighbourLevel) { + this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex; + } + } + } + + final int currentChunkOffX = chunkX << 4; + final int currentChunkOffZ = chunkZ << 4; + final int neighbourChunkOffX = (chunkX + direction.x) << 4; + final int neighbourChunkOffZ = (chunkZ + direction.z) << 4; + final int chunkOffY = chunkY << 4; + for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) { + // try to queue neighbouring data together + // index = x | (z << 4) | (y << 8) + if (i < centerDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesCenter[i]; + this.checkBlock(lightAccess, currentChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + currentChunkOffZ | ((value >>> 4) & 0xF)); + } + if (i < neighbourDelayedChecks) { + final int value = this.chunkCheckDelayedUpdatesNeighbour[i]; + this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15), + chunkOffY | (value >>> 8), + neighbourChunkOffZ | ((value >>> 4) & 0xF)); + } + } + } + } + + protected void checkChunkEdges(final ILightAccess lightAccess, final IChunkAccess chunk, final ShortCollection sections) { + final ChunkCoordIntPair chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // verifies that light levels on this chunks edges are consistent with this chunk's neighbours + // edges. if they are not, they are decreased (effectively performing the logic in checkBlock). + // This does not resolve skylight source problems. + protected void checkChunkEdges(final ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkCoordIntPair chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ); + } + + this.performLightDecrease(lightAccess); + } + + // pulls light from neighbours, and adds them into the increase queue. does not actually propagate. + protected final void propagateNeighbourLevels(final ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, final int toSection) { + final ChunkCoordIntPair chunkPos = chunk.getPos(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + + for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) { + final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ); + if (currNibble == null) { + continue; + } + for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) { + final int neighbourOffX = direction.x; + final int neighbourOffZ = direction.z; + + final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX, + currSectionY, chunkZ + neighbourOffZ); + + if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) { + // can't pull from 0 + continue; + } + + // neighbour chunk + final int incX; + final int incZ; + final int startX; + final int startZ; + + if (neighbourOffX != 0) { + // x direction + incX = 0; + incZ = 1; + + if (direction.x < 0) { + // negative + startX = (chunkX << 4) - 1; + } else { + startX = (chunkX << 4) + 16; + } + startZ = chunkZ << 4; + } else { + // z direction + incX = 1; + incZ = 0; + + if (neighbourOffZ < 0) { + // negative + startZ = (chunkZ << 4) - 1; + } else { + startZ = (chunkZ << 4) + 16; + } + startX = chunkX << 4; + } + + final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk + final int encodeOffset = this.coordinateOffset; + + for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) { + for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) { + final int level = neighbourNibble.getUpdating( + (currX & 15) + | ((currZ & 15) << 4) + | ((currY & 15) << 8) + ); + + if (level <= 1) { + // nothing to propagate + continue; + } + + this.appendToIncreaseQueue( + ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((level & 0xFL) << (6 + 6 + 16)) + | (propagateDirection << (6 + 6 + 16 + 4)) + | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check. + ); + } + } + } + } + } + + public static Boolean[] getEmptySectionsForChunk(final IChunkAccess chunk) { + final ChunkSection[] sections = chunk.getSections(); + final Boolean[] ret = new Boolean[sections.length]; + + for (int i = 0; i < sections.length; ++i) { + if (sections[i] == null || sections[i].isFullOfAir()) { + ret[i] = Boolean.TRUE; + } else { + ret[i] = Boolean.FALSE; + } + } + + return ret; + } + + public final void forceHandleEmptySectionChanges(final ILightAccess lightAccess, final IChunkAccess chunk, + final Boolean[] emptinessChanges) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void handleEmptySectionChanges(final ILightAccess lightAccess, final int chunkX, final int chunkZ, + final Boolean[] emptinessChanges) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + try { + if (this.isClientSide) { + // force current chunk into cache + final IChunkAccess chunk = (IChunkAccess)lightAccess.getFeaturesReadyChunk(chunkX, chunkZ); + if (chunk == null) { + // unloaded this frame (or last), and we were still queued + return; + } + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk)); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + } + final IChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // subclasses are guaranteed that this is always called before a changed block set + // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks + // rets non-null when the emptiness map changed and needs to be updated + protected abstract boolean[] handleEmptySectionChanges(final ILightAccess lightAccess, final IChunkAccess chunk, + final Boolean[] emptinessChanges, final boolean unlit); + + public final void checkChunkEdges(final ILightAccess lightAccess, final int chunkX, final int chunkZ) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final IChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void checkChunkEdges(final ILightAccess lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false); + try { + final IChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ); + if (chunk == null) { + return; + } + this.checkChunkEdges(lightAccess, chunk, sections); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + // subclasses should not initialise caches, as this will always be done by the super call + // subclasses should not invoke updateVisible, as this will always be done by the super call + // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current + // chunks light values with respect to neighbours + // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function + // does not need to detect empty chunks itself (and it should do no handling for them either!) + protected abstract void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks); + + public final void light(final ILightAccess lightAccess, final IChunkAccess chunk, final Boolean[] emptySections) { + final int chunkX = chunk.getPos().x; + final int chunkZ = chunk.getPos().z; + this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true); + + try { + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1); + // force current chunk into cache + this.setChunkInCache(chunkX, chunkZ, chunk); + this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections()); + this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles); + this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk)); + + final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true); + if (ret != null) { + this.setEmptinessMap(chunk, ret); + } + this.lightChunk(lightAccess, chunk, true); // TODO + this.setNibbles(chunk, nibbles); + this.updateVisible(lightAccess); + } finally { + this.destroyCaches(); + } + } + + public final void relightChunks(final ILightAccess lightAccess, final Set chunks, + final Consumer chunkLightCallback, final IntConsumer onComplete) { + // it's recommended for maximum performance that the set is ordered according to a BFS from the center of + // the region of chunks to relight + // it's required that tickets are added for each chunk to keep them loaded + final Long2ObjectOpenHashMap nibblesByChunk = new Long2ObjectOpenHashMap<>(); + final Long2ObjectOpenHashMap emptinessMapByChunk = new Long2ObjectOpenHashMap<>(); + + final int[] neighbourLightOrder = new int[] { + // d = 0 + 0, 0, + // d = 1 + -1, 0, + 0, -1, + 1, 0, + 0, 1, + // d = 2 + -1, 1, + 1, 1, + -1, -1, + 1, -1, + }; + + int lightCalls = 0; + + for (final ChunkCoordIntPair chunkPos : chunks) { + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + final IChunkAccess chunk = (IChunkAccess) lightAccess.getFeaturesReadyChunk(chunkX, chunkZ); + if (chunk == null || !this.canUseChunk(chunk)) { + throw new IllegalStateException(); + } + + for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) { + final int dx = neighbourLightOrder[i]; + final int dz = neighbourLightOrder[i + 1]; + final int neighbourX = dx + chunkX; + final int neighbourZ = dz + chunkZ; + + final IChunkAccess neighbour = (IChunkAccess) lightAccess.getFeaturesReadyChunk(neighbourX, neighbourZ); + if (neighbour == null || !this.canUseChunk(neighbour)) { + continue; + } + + if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) { + // lit already called for neighbour, no need to light it now + continue; + } + + // light neighbour chunk + this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7); + try { + // insert all neighbouring chunks for this neighbour that we have data for + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + final int neighbourX2 = neighbourX + dx2; + final int neighbourZ2 = neighbourZ + dz2; + final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2); + final IChunkAccess neighbour2 = (IChunkAccess)lightAccess.getFeaturesReadyChunk(neighbourX2, neighbourZ2); + if (neighbour2 == null || !this.canUseChunk(neighbour2)) { + continue; + } + + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key); + if (nibbles == null) { + // we haven't lit this chunk + continue; + } + + this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2); + this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections()); + this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles); + this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key)); + } + } + + final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + // now insert the neighbour chunk and light it + final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world); + nibblesByChunk.put(key, nibbles); + + this.setChunkInCache(neighbourX, neighbourZ, neighbour); + this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections()); + this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles); + + final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true); + emptinessMapByChunk.put(key, neighbourEmptiness); + if (chunks.contains(new ChunkCoordIntPair(neighbourX, neighbourZ))) { + this.setEmptinessMap(neighbour, neighbourEmptiness); + } + + this.lightChunk(lightAccess, neighbour, false); + } finally { + this.destroyCaches(); + } + } + + // done lighting all neighbours, so the chunk is now fully lit + + // make sure nibbles are fully updated before calling back + final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + for (final SWMRNibbleArray nibble : nibbles) { + nibble.updateVisible(); + } + + this.setNibbles(chunk, nibbles); + + for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { + lightAccess.markLightSectionDirty(this.skylightPropagator ? EnumSkyBlock.SKY : EnumSkyBlock.BLOCK, new SectionPosition(chunkX, y, chunkX)); + } + + // now do callback + if (chunkLightCallback != null) { + chunkLightCallback.accept(chunkPos); + } + ++lightCalls; + } + + if (onComplete != null) { + onComplete.accept(lightCalls); + } + } + + // old algorithm for propagating + // this is also the basic algorithm, the optimised algorithm is always going to be tested against this one + // and this one is always tested against vanilla + // contains: + // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6)))) + // next 4 bits: propagated light level (0, 15] + // next 6 bits: propagation direction bitset + // next 24 bits: unused + // last 4 bits: state flags + // state flags: + // whether the propagation must set the current position's light value (0 if decrease, propagated light level if increase) + // whether the propagation needs to check if its current level is equal to the expected level + // used only in increase propagation + protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1; + // whether the propagation needs to consider if its block is conditionally transparent + protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE; + + protected long[] increaseQueue = new long[16 * 16 * 16]; + protected int increaseQueueInitialLength; + protected long[] decreaseQueue = new long[16 * 16 * 16]; + protected int decreaseQueueInitialLength; + + protected final long[] resizeIncreaseQueue() { + return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2); + } + + protected final long[] resizeDecreaseQueue() { + return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2); + } + + protected final void appendToIncreaseQueue(final long value) { + final int idx = this.increaseQueueInitialLength++; + long[] queue = this.increaseQueue; + if (idx >= queue.length) { + queue = this.resizeIncreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected final void appendToDecreaseQueue(final long value) { + final int idx = this.decreaseQueueInitialLength++; + long[] queue = this.decreaseQueue; + if (idx >= queue.length) { + queue = this.resizeDecreaseQueue(); + queue[idx] = value; + } else { + queue[idx] = value; + } + } + + protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][]; + protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1; + static { + for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) { + final List directions = new ArrayList<>(); + for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) { + directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]); + } + OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]); + } + } + + protected final void performLightIncrease(final ILightAccess lightAccess) { + final IBlockAccess world = lightAccess.getWorld(); + long[] queue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.increaseQueueInitialLength; + this.increaseQueueInitialLength = 0; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)]; + + if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) { + if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) { + // not at the level we expect, so something changed. + continue; + } + } + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want or unloaded + } + + final IBlockData blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.setValues(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms); + + if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getOpacity(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } else { + // we actually need to worry about our state here + final IBlockData fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.setValues(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getCullingFace(world, this.mutablePos2, propagate.nms) : VoxelShapes.getEmptyShape(); + + if (fromShape != VoxelShapes.getEmptyShape() && VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), fromShape)) { + continue; + } + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int currentLevel; + + if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) { + continue; // already at the level we want + } + + final IBlockData blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached); + if (targetLevel > currentLevel) { + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + continue; + } + } + continue; + } else { + this.mutablePos1.setValues(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms); + + if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getOpacity(world, this.mutablePos1); + final int targetLevel = propagatedLightLevel - Math.max(1, opacity); + if (targetLevel <= currentLevel) { + continue; + } + + currentNibble.set(localIndex, targetLevel); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 1) { + if (queueLength >= queue.length) { + queue = this.resizeIncreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)) + | (flags); + } + continue; + } + } + } + } + } + + protected final void performLightDecrease(final ILightAccess lightAccess) { + final IBlockAccess world = lightAccess.getWorld(); + long[] queue = this.decreaseQueue; + long[] increaseQueue = this.increaseQueue; + int queueReadIndex = 0; + int queueLength = this.decreaseQueueInitialLength; + this.decreaseQueueInitialLength = 0; + int increaseQueueLength = this.increaseQueueInitialLength; + final int decodeOffsetX = -this.encodeOffsetX; + final int decodeOffsetY = -this.encodeOffsetY; + final int decodeOffsetZ = -this.encodeOffsetZ; + final int encodeOffset = this.coordinateOffset; + final int sectionOffset = this.chunkSectionIndexOffset; + final int emittedMask = this.emittedLightMask; + final VariableBlockLightHandler customLightHandler = this.skylightPropagator ? null : ((WorldServer)world).customBlockLightHandlers; + + while (queueReadIndex < queueLength) { + final long queueValue = queue[queueReadIndex++]; + + final int posX = ((int)queueValue & 63) + decodeOffsetX; + final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ; + final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY; + final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF); + final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)]; + + if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) { + // we don't need to worry about our state here. + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final IBlockData blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0L); + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.setValues(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms); + + if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getOpacity(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | flags; + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } else { + // we actually need to worry about our state here + final IBlockData fromBlock = this.getBlockState(posX, posY, posZ); + this.mutablePos2.setValues(posX, posY, posZ); + for (final AxisDirection propagate : checkDirections) { + final int offX = posX + propagate.x; + final int offY = posY + propagate.y; + final int offZ = posZ + propagate.z; + + final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; + final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8); + + final VoxelShape fromShape = (fromBlock.isConditionallyFullOpaque()) ? fromBlock.getCullingFace(world, this.mutablePos2, propagate.nms) : VoxelShapes.getEmptyShape(); + + if (fromShape != VoxelShapes.getEmptyShape() && VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), fromShape)) { + continue; + } + + final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex]; + final int lightLevel; + + if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) { + // already at lowest (or unloaded), nothing we can do + continue; + } + + final IBlockData blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.getOpacityIfCached(); + if (opacityCached != -1) { + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | FLAG_RECHECK_LEVEL; + continue; + } + final int emittedLight = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0L); + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + continue; + } + continue; + } else { + this.mutablePos1.setValues(offX, offY, offZ); + long flags = 0; + if (blockState.isConditionallyFullOpaque()) { + final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms); + + if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) { + continue; + } + flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS; + } + + final int opacity = blockState.getOpacity(world, this.mutablePos1); + final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity)); + if (lightLevel > targetLevel) { + // it looks like another source propagated here, so re-propagate it + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((lightLevel & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (FLAG_RECHECK_LEVEL | flags); + continue; + } + final int emittedLight = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask; + if (emittedLight != 0) { + // re-propagate source + if (increaseQueueLength >= increaseQueue.length) { + increaseQueue = this.resizeIncreaseQueue(); + } + increaseQueue[increaseQueueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | flags; + } + + currentNibble.set(localIndex, emittedLight); + this.postLightUpdate(offX, offY, offZ); + + if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour... + if (queueLength >= queue.length) { + queue = this.resizeDecreaseQueue(); + } + queue[queueLength++] = + ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)) + | flags; + } + continue; + } + } + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performLightIncrease(lightAccess); + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..518c21b96947cb87bcc3b5fc3f6210bcb0944e33 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java @@ -0,0 +1,490 @@ +package com.tuinity.tuinity.chunk.light; + +import com.tuinity.tuinity.util.CoordinateUtils; +import com.tuinity.tuinity.util.WorldUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import net.minecraft.server.BlockPosition; +import net.minecraft.server.ChunkCoordIntPair; +import net.minecraft.server.ChunkStatus; +import net.minecraft.server.IChunkAccess; +import net.minecraft.server.ILightAccess; +import net.minecraft.server.LightEngineLayerEventListener; +import net.minecraft.server.NibbleArray; +import net.minecraft.server.SectionPosition; +import net.minecraft.server.WorldServer; +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public final class StarLightInterface { + + + /** + * Can be {@code null}, indicating the light is all empty. + */ + protected final WorldServer world; + protected final ILightAccess lightAccess; + + protected final ArrayDeque cachedSkyPropagators; + protected final ArrayDeque cachedBlockPropagators; + + protected final Long2ObjectOpenHashMap changedBlocks = new Long2ObjectOpenHashMap<>(); + + protected final LightEngineLayerEventListener skyReader; + protected final LightEngineLayerEventListener blockReader; + protected static final boolean isClientSide = false; + + protected final int minSection; + protected final int maxSection; + protected final int minLightSection; + protected final int maxLightSection; + + public StarLightInterface(final ILightAccess lightAccess, final boolean hasSkyLight, final boolean hasBlockLight) { + this.lightAccess = lightAccess; + this.world = lightAccess == null ? null : (WorldServer)lightAccess.getWorld(); + this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; + this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; + if (this.world == null) { + this.minSection = 0; + this.maxSection = 15; + this.minLightSection = -1; + this.maxLightSection = 16; + } else { + this.minSection = WorldUtil.getMinSection(this.world); + this.maxSection = WorldUtil.getMaxSection(this.world); + this.minLightSection = WorldUtil.getMinLightSection(this.world); + this.maxLightSection = WorldUtil.getMaxLightSection(this.world); + } + this.skyReader = !hasSkyLight ? LightEngineLayerEventListener.Void.INSTANCE : new LightEngineLayerEventListener() { + @Override + public NibbleArray a(final SectionPosition pos) { + final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLit()) || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) { + return null; + } + + final int sectionY = pos.getY(); + + if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { + return null; + } + + if (chunk.getEmptinessMap() == null) { + return null; + } + + return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); + } + + @Override + public int b(final BlockPosition blockPos) { + final int x = blockPos.getX(); + int y = blockPos.getY(); + final int z = blockPos.getZ(); + + final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4); + if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLit()) || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) { + return 15; + } + + int sectionY = y >> 4; + + if (sectionY > StarLightInterface.this.maxLightSection) { + return 15; + } + + if (sectionY < StarLightInterface.this.minLightSection) { + sectionY = StarLightInterface.this.minLightSection; + y = sectionY << 4; + } + + final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles(); + final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection]; + + if (StarLightInterface.this.isClientSide) { + if (!immediate.isNullNibbleUpdating()) { + return immediate.getUpdating(x, y, z); + } + } else { + if (!immediate.isNullNibbleVisible()) { + return immediate.getVisible(x, y, z); + } + } + + final boolean[] emptinessMap = chunk.getEmptinessMap(); + + if (emptinessMap == null) { + return 15; + } + + // are we above this chunk's lowest empty section? + int lowestY = StarLightInterface.this.minLightSection - 1; + for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) { + if (emptinessMap[currY - StarLightInterface.this.minSection]) { + continue; + } + + // should always be full lit here + lowestY = currY; + break; + } + + if (sectionY > lowestY) { + return 15; + } + + // this nibble is going to depend solely on the skylight data above it + // find first non-null data above (there does exist one, as we just found it above) + for (int currY = sectionY + 1; currY <= StarLightInterface.this.maxLightSection; ++currY) { + final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection]; + if (StarLightInterface.this.isClientSide) { + if (!nibble.isNullNibbleUpdating()) { + return nibble.getUpdating(x, 0, z); + } + } else { + if (!nibble.isNullNibbleVisible()) { + return nibble.getVisible(x, 0, z); + } + } + } + + // should never reach here + return 15; + } + + @Override + public void a(final SectionPosition pos, final boolean notReady) { + StarLightInterface.this.sectionChange(pos, notReady); + } + }; + this.blockReader = !hasBlockLight ? LightEngineLayerEventListener.Void.INSTANCE : new LightEngineLayerEventListener() { + @Override + public NibbleArray a(final SectionPosition pos) { + final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); + + if (pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { + return null; + } + + return chunk != null ? chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble() : null; + } + + @Override + public int b(final BlockPosition blockPos) { + final int cx = blockPos.getX() >> 4; + final int cy = blockPos.getY() >> 4; + final int cz = blockPos.getZ() >> 4; + + if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) { + return 0; + } + + final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz); + + if (chunk == null) { + return 0; + } + + final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection]; + if (StarLightInterface.this.isClientSide) { + return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } else { + return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ()); + } + } + + @Override + public void a(final SectionPosition pos, final boolean notReady) { + return; // block engine doesn't care + } + }; + } + + public LightEngineLayerEventListener getSkyReader() { + return this.skyReader; + } + + public LightEngineLayerEventListener getBlockReader() { + return this.blockReader; + } + + public boolean isClientSide() { + return this.isClientSide; + } + + public IChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { + if (this.world == null) { + // empty world + return null; + } + return this.world.getChunkProvider().getChunkAtImmediately(chunkX, chunkZ); + } + + public boolean hasUpdates() { + synchronized (this) { + return !this.changedBlocks.isEmpty(); + } + } + + public WorldServer getWorld() { + return this.world; + } + + public ILightAccess getLightAccess() { + return this.lightAccess; + } + + protected final SkyStarLightEngine getSkyLightEngine() { + if (this.cachedSkyPropagators == null) { + return null; + } + final SkyStarLightEngine ret; + synchronized (this.cachedSkyPropagators) { + ret = this.cachedSkyPropagators.pollFirst(); + } + + if (ret == null) { + return new SkyStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { + if (this.cachedSkyPropagators == null) { + return; + } + synchronized (this.cachedSkyPropagators) { + this.cachedSkyPropagators.addFirst(engine); + } + } + + protected final BlockStarLightEngine getBlockLightEngine() { + if (this.cachedBlockPropagators == null) { + return null; + } + final BlockStarLightEngine ret; + synchronized (this.cachedBlockPropagators) { + ret = this.cachedBlockPropagators.pollFirst(); + } + + if (ret == null) { + return new BlockStarLightEngine(this.world); + } + return ret; + } + + protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { + if (this.cachedBlockPropagators == null) { + return; + } + synchronized (this.cachedBlockPropagators) { + this.cachedBlockPropagators.addFirst(engine); + } + } + + public void blockChange(BlockPosition pos) { + if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world + return; + } + + pos = pos.immutableCopy(); + synchronized (this.changedBlocks) { + this.changedBlocks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { + return new ChunkChanges(); + }).changedPositions.add(pos); + } + } + + public void sectionChange(final SectionPosition pos, final boolean newEmptyValue) { + if (this.world == null) { // empty world + return; + } + + synchronized (this.changedBlocks) { + final ChunkChanges changes = this.changedBlocks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { + return new ChunkChanges(); + }); + if (changes.changedSectionSet == null) { + changes.changedSectionSet = new Boolean[this.maxSection - this.minSection + 1]; + } + changes.changedSectionSet[pos.getY() - this.minSection] = Boolean.valueOf(newEmptyValue); + } + } + + public void forceLoadInChunk(final IChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + if (blockEngine != null) { + blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void lightChunk(final IChunkAccess chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.light(this.lightAccess, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.light(this.lightAccess, chunk, emptySections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void relightChunks(final Set chunks, final Consumer chunkLightCallback, + final IntConsumer onComplete) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, + blockEngine == null ? onComplete : null); + } + if (blockEngine != null) { + blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkChunkEdges(final int chunkX, final int chunkZ) { + this.checkSkyEdges(chunkX, chunkZ); + this.checkBlockEdges(chunkX, chunkZ); + } + + public void checkSkyEdges(final int chunkX, final int chunkZ) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + + try { + if (skyEngine != null) { + skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseSkyLightEngine(skyEngine); + } + } + + public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + try { + if (blockEngine != null) { + blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); + } + } finally { + this.releaseBlockLightEngine(blockEngine); + } + } + + public void propagateChanges() { + synchronized (this.changedBlocks) { + if (this.changedBlocks.isEmpty()) { + return; + } + } + final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); + final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); + + try { + // TODO be smarter about this in the future + final Long2ObjectOpenHashMap changedBlocks; + synchronized (this.changedBlocks) { + changedBlocks = this.changedBlocks.clone(); + this.changedBlocks.clear(); + } + + for (final Iterator> iterator = changedBlocks.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + final Long2ObjectMap.Entry entry = iterator.next(); + final long coordinate = entry.getLongKey(); + final ChunkChanges changes = entry.getValue(); + final Set positions = changes.changedPositions; + final Boolean[] sectionChanges = changes.changedSectionSet; + + final int chunkX = CoordinateUtils.getChunkX(coordinate); + final int chunkZ = CoordinateUtils.getChunkZ(coordinate); + + if (skyEngine != null) { + skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + if (blockEngine != null) { + blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); + } + } + } finally { + this.releaseSkyLightEngine(skyEngine); + this.releaseBlockLightEngine(blockEngine); + } + } + + protected static final class ChunkChanges { + + // note: on the main thread, empty section changes are queued before block changes. This means we don't need + // to worry about cases where a block change is called inside an empty chunk section, according to the "emptiness" map per chunk, + // for example. + public final Set changedPositions = new HashSet<>(); + + public Boolean[] changedSectionSet; + + } +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b8a6c59ee3c919e47e4be76fc4e1737d81a5810b --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java @@ -0,0 +1,30 @@ +package com.tuinity.tuinity.chunk.light; + +import net.minecraft.server.BlockPosition; +import java.util.Collection; + +/** + * Recommended implementation is {@link VariableBlockLightHandlerImpl}, but you can implement this interface yourself + * if you want. + */ +public interface VariableBlockLightHandler { + + /** + * Returns the custom light level for the specified position. Must return {@code -1} if there is custom level. + * @param x Block x world coordinate + * @param y Block y world coordinate + * @param z Block z world coordinate + * @return Custom light level for the specified position + */ + public int getLightLevel(final int x, final int y, final int z); + + /** + * Returns a collection of all the custom light positions inside the specified chunk. This must be fast, + * as it is used during chunk lighting. + * @param chunkX Chunk's x coordinate. + * @param chunkZ Chunk's z coordinate. + * @return Collection of all the custom light positions in the specified chunk. + */ + public Collection getCustomLightPositions(final int chunkX, final int chunkZ); + +} diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..125f59826a9b0174039139ed0715a4ed3df3724b --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java @@ -0,0 +1,112 @@ +package com.tuinity.tuinity.chunk.light; + +import com.tuinity.tuinity.util.CoordinateUtils; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.server.BlockPosition; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.StampedLock; + +public class VariableBlockLightHandlerImpl implements VariableBlockLightHandler { + + protected final Long2ObjectOpenHashMap> positionsByChunk = new Long2ObjectOpenHashMap<>(); + protected final Long2IntOpenHashMap lightValuesByPosition = new Long2IntOpenHashMap(); + protected final StampedLock seqlock = new StampedLock(); + { + this.lightValuesByPosition.defaultReturnValue(-1); + this.positionsByChunk.defaultReturnValue(Collections.emptySet()); + } + + @Override + public int getLightLevel(final int x, final int y, final int z) { + final long key = CoordinateUtils.getBlockKey(x, y, z); + try { + final long attempt = this.seqlock.tryOptimisticRead(); + if (attempt != 0L) { + final int ret = this.lightValuesByPosition.get(key); + + if (this.seqlock.validate(attempt)) { + return ret; + } + } + } catch (final Error error) { + throw error; + } catch (final Throwable thr) { + // ignore + } + + this.seqlock.readLock(); + try { + return this.lightValuesByPosition.get(key); + } finally { + this.seqlock.tryUnlockRead(); + } + } + + @Override + public Collection getCustomLightPositions(final int chunkX, final int chunkZ) { + final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); + try { + final long attempt = this.seqlock.tryOptimisticRead(); + if (attempt != 0L) { + final Set ret = new HashSet<>(this.positionsByChunk.get(key)); + + if (this.seqlock.validate(attempt)) { + return ret; + } + } + } catch (final Error error) { + throw error; + } catch (final Throwable thr) { + // ignore + } + + this.seqlock.readLock(); + try { + return new HashSet<>(this.positionsByChunk.get(key)); + } finally { + this.seqlock.tryUnlockRead(); + } + } + + public void setSource(final int x, final int y, final int z, final int to) { + if (to < 0 || to > 15) { + throw new IllegalArgumentException(); + } + this.seqlock.writeLock(); + try { + if (this.lightValuesByPosition.put(CoordinateUtils.getBlockKey(x, y, z), to) == -1) { + this.positionsByChunk.computeIfAbsent(CoordinateUtils.getChunkKey(x >> 4, z >> 4), (final long keyInMap) -> { + return new HashSet<>(); + }).add(new BlockPosition(x, y, z)); + } + } finally { + this.seqlock.tryUnlockWrite(); + } + } + + public int removeSource(final int x, final int y, final int z) { + this.seqlock.writeLock(); + try { + final int ret = this.lightValuesByPosition.remove(CoordinateUtils.getBlockKey(x, y, z)); + + if (ret != -1) { + final long chunkKey = CoordinateUtils.getChunkKey(x >> 4, z >> 4); + + final Set positions = this.positionsByChunk.get(chunkKey); + positions.remove(new BlockPosition(x, y, z)); + + if (positions.isEmpty()) { + this.positionsByChunk.remove(chunkKey); + } + } + + return ret; + } finally { + this.seqlock.tryUnlockWrite(); + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..42ce3b80217b574a1852e12f500b366a912e23e2 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java @@ -0,0 +1,381 @@ +package com.tuinity.tuinity.config; + +import com.destroystokyo.paper.util.SneakyThrow; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TicketType; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import java.io.File; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.logging.Level; + +public final class TuinityConfig { + + public static final String CONFIG_HEADER = "Configuration file for Tuinity."; + public static final int CURRENT_CONFIG_VERSION = 2; + + private static final Object[] EMPTY = new Object[0]; + + private static File configFile; + public static YamlConfiguration config; + private static int configVersion; + public static boolean createWorldSections = true; + + public static void init(final File file) { + // TODO remove this in the future... + final File tuinityConfig = new File(file.getParent(), "tuinity.yml"); + if (!tuinityConfig.exists()) { + final File oldConfig = new File(file.getParent(), "concrete.yml"); + oldConfig.renameTo(tuinityConfig); + } + TuinityConfig.configFile = file; + final YamlConfiguration config = new YamlConfiguration(); + config.options().header(CONFIG_HEADER); + config.options().copyDefaults(true); + + if (!file.exists()) { + try { + file.createNewFile(); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Failure to create tuinity config", ex); + } + } else { + try { + config.load(file); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Failure to load tuinity config", ex); + SneakyThrow.sneaky(ex); /* Rethrow, this is critical */ + throw new RuntimeException(ex); // unreachable + } + } + + TuinityConfig.load(config); + } + + public static void load(final YamlConfiguration config) { + TuinityConfig.config = config; + TuinityConfig.configVersion = TuinityConfig.getInt("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION); + TuinityConfig.set("config-version-please-do-not-modify-me", CURRENT_CONFIG_VERSION); + + for (final Method method : TuinityConfig.class.getDeclaredMethods()) { + if (method.getReturnType() != void.class || method.getParameterCount() != 0 || + !Modifier.isPrivate(method.getModifiers()) || !Modifier.isStatic(method.getModifiers())) { + continue; + } + + try { + method.setAccessible(true); + method.invoke(null, EMPTY); + } catch (final Exception ex) { + SneakyThrow.sneaky(ex); /* Rethrow, this is critical */ + throw new RuntimeException(ex); // unreachable + } + } + + /* We re-save to add new options */ + try { + config.save(TuinityConfig.configFile); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex); + } + } + + static void set(final String path, final Object value) { + TuinityConfig.config.set(path, value); + } + + static boolean getBoolean(final String path, final boolean dfl) { + TuinityConfig.config.addDefault(path, Boolean.valueOf(dfl)); + return TuinityConfig.config.getBoolean(path, dfl); + } + + static int getInt(final String path, final int dfl) { + TuinityConfig.config.addDefault(path, Integer.valueOf(dfl)); + return TuinityConfig.config.getInt(path, dfl); + } + + static long getLong(final String path, final long dfl) { + TuinityConfig.config.addDefault(path, Long.valueOf(dfl)); + return TuinityConfig.config.getLong(path, dfl); + } + + static double getDouble(final String path, final double dfl) { + TuinityConfig.config.addDefault(path, Double.valueOf(dfl)); + return TuinityConfig.config.getDouble(path, dfl); + } + + static String getString(final String path, final String dfl) { + TuinityConfig.config.addDefault(path, dfl); + return TuinityConfig.config.getString(path, dfl); + } + + public static boolean tickWorldsInParallel; + + /** + * if tickWorldsInParallel == true, then this value is used as a default only for worlds + */ + public static int tickThreads; + + /* + private static void worldticking() { + tickWorldsInParallel = TuinityConfig.getBoolean("tick-worlds-in-parallel", false); + tickThreads = TuinityConfig.getInt("server-tick-threads", 1); // will be 4 in the future + }*/ + + public static int delayChunkUnloadsBy; + + private static void delayChunkUnloadsBy() { + delayChunkUnloadsBy = TuinityConfig.getInt("delay-chunkunloads-by", 5) * 20; + if (delayChunkUnloadsBy >= 0) { + TicketType.DELAYED_UNLOAD.loadPeriod = delayChunkUnloadsBy; + } + } + + public static boolean lagCompensateBlockBreaking; + + private static void lagCompensateBlockBreaking() { + lagCompensateBlockBreaking = TuinityConfig.getBoolean("lag-compensate-block-breaking", true); + } + + public static final class PacketLimit { + public final double packetLimitInterval; + public final double maxPacketRate; + public final ViolateAction violateAction; + + public PacketLimit(final double packetLimitInterval, final double maxPacketRate, final ViolateAction violateAction) { + this.packetLimitInterval = packetLimitInterval; + this.maxPacketRate = maxPacketRate; + this.violateAction = violateAction; + } + + public static enum ViolateAction { + KICK, DROP; + } + } + + public static String kickMessage; + public static PacketLimit allPacketsLimit; + public static java.util.Map>, PacketLimit> packetSpecificLimits = new java.util.HashMap<>(); + + private static void packetLimiter() { + packetSpecificLimits.clear(); + kickMessage = org.bukkit.ChatColor.translateAlternateColorCodes('&', TuinityConfig.getString("packet-limiter.kick-message", "&cSent too many packets")); + allPacketsLimit = new PacketLimit( + TuinityConfig.getDouble("packet-limiter.limits.all.interval", 7.0), + TuinityConfig.getDouble("packet-limiter.limits.all.max-packet-rate", 500.0), + PacketLimit.ViolateAction.KICK + ); + if (allPacketsLimit.maxPacketRate <= 0.0 || allPacketsLimit.packetLimitInterval <= 0.0) { + allPacketsLimit = null; + } + final ConfigurationSection section = TuinityConfig.config.getConfigurationSection("packet-limiter.limits"); + + // add default packets + + // auto recipe limiting + TuinityConfig.getDouble("packet-limiter.limits." + + net.minecraft.server.PacketPlayInAutoRecipe.class.getSimpleName() + ".interval", 4.0); + TuinityConfig.getDouble("packet-limiter.limits." + + net.minecraft.server.PacketPlayInAutoRecipe.class.getSimpleName() + ".max-packet-rate", 5.0); + TuinityConfig.getString("packet-limiter.limits." + + net.minecraft.server.PacketPlayInAutoRecipe.class.getSimpleName() + ".action", PacketLimit.ViolateAction.DROP.name()); + + for (final String packetClassName : section.getKeys(false)) { + if (packetClassName.equals("all")) { + continue; + } + final Class packetClazz; + + try { + packetClazz = Class.forName("net.minecraft.server." + packetClassName); + } catch (final ClassNotFoundException ex) { + MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml"); + continue; + } + + if (!net.minecraft.server.Packet.class.isAssignableFrom(packetClazz)) { + MinecraftServer.LOGGER.warn("Packet '" + packetClassName + "' does not exist, cannot limit it! Please update tuinity.yml"); + continue; + } + + if (!(section.get(packetClassName.concat(".interval")) instanceof Number) || !(section.get(packetClassName.concat(".max-packet-rate")) instanceof Number)) { + throw new RuntimeException("Packet limit setting " + packetClassName + " is missing interval or max-packet-rate!"); + } + + final String actionString = section.getString(packetClassName.concat(".action"), "KICK"); + PacketLimit.ViolateAction action = PacketLimit.ViolateAction.KICK; + for (PacketLimit.ViolateAction test : PacketLimit.ViolateAction.values()) { + if (actionString.equalsIgnoreCase(test.name())) { + action = test; + break; + } + } + + final double interval = section.getDouble(packetClassName.concat(".interval")); + final double rate = section.getDouble(packetClassName.concat(".max-packet-rate")); + + if (interval > 0.0 && rate > 0.0) { + packetSpecificLimits.put((Class)packetClazz, new PacketLimit(interval, rate, action)); + } + } + } + + public static boolean useNewLightEngine; + + private static void useNewLightEngine() { + useNewLightEngine = TuinityConfig.getBoolean("use-new-light-engine", true); + } + + public static final class WorldConfig { + + public final String worldName; + public String configPath; + ConfigurationSection worldDefaults; + + public WorldConfig(final String worldName) { + this.worldName = worldName; + this.init(); + } + + public void init() { + this.worldDefaults = TuinityConfig.config.getConfigurationSection("world-settings.default"); + if (this.worldDefaults == null) { + this.worldDefaults = TuinityConfig.config.createSection("world-settings.default"); + } + + String worldSectionPath = TuinityConfig.configVersion < 1 ? this.worldName : "world-settings.".concat(this.worldName); + ConfigurationSection section = TuinityConfig.config.getConfigurationSection(worldSectionPath); + this.configPath = worldSectionPath; + if (TuinityConfig.createWorldSections) { + if (section == null) { + section = TuinityConfig.config.createSection(worldSectionPath); + } + TuinityConfig.config.set(worldSectionPath, section); + } + + this.load(); + } + + public void load() { + for (final Method method : TuinityConfig.WorldConfig.class.getDeclaredMethods()) { + if (method.getReturnType() != void.class || method.getParameterCount() != 0 || + !Modifier.isPrivate(method.getModifiers()) || Modifier.isStatic(method.getModifiers())) { + continue; + } + + try { + method.setAccessible(true); + method.invoke(this, EMPTY); + } catch (final Exception ex) { + SneakyThrow.sneaky(ex); /* Rethrow, this is critical */ + throw new RuntimeException(ex); // unreachable + } + } + + if (TuinityConfig.configVersion < 1) { + ConfigurationSection oldSection = TuinityConfig.config.getConfigurationSection(this.worldName); + TuinityConfig.config.set("world-settings.".concat(this.worldName), oldSection); + TuinityConfig.config.set(this.worldName, null); + } + + /* We re-save to add new options */ + try { + TuinityConfig.config.save(TuinityConfig.configFile); + } catch (final Exception ex) { + Bukkit.getLogger().log(Level.SEVERE, "Unable to save tuinity config", ex); + } + } + + /** + * update world defaults for the specified path, but also sets this world's config value for the path + * if it exists + */ + void set(final String path, final Object val) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.set(path, val); + if (config != null && config.get(path) != null) { + config.set(path, val); + } + } + + boolean getBoolean(final String path, final boolean dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Boolean.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getBoolean(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getBoolean(path) : config.getBoolean(path, this.worldDefaults.getBoolean(path)); + } + + int getInt(final String path, final int dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Integer.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getInt(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getInt(path) : config.getInt(path, this.worldDefaults.getInt(path)); + } + + long getLong(final String path, final long dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Long.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getLong(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getLong(path) : config.getLong(path, this.worldDefaults.getLong(path)); + } + + double getDouble(final String path, final double dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, Double.valueOf(dfl)); + if (TuinityConfig.configVersion < 1) { + if (config != null && config.getDouble(path) == dfl) { + config.set(path, null); + } + } + return config == null ? this.worldDefaults.getDouble(path) : config.getDouble(path, this.worldDefaults.getDouble(path)); + } + + String getString(final String path, final String dfl) { + final ConfigurationSection config = TuinityConfig.config.getConfigurationSection(this.configPath); + this.worldDefaults.addDefault(path, dfl); + return config == null ? this.worldDefaults.getString(path) : config.getString(path, this.worldDefaults.getString(path)); + } + + /** ignored if {@link TuinityConfig#tickWorldsInParallel} == false */ + public int threads; + + /* + private void worldthreading() { + final int threads = this.getInt("tick-threads", -1); + this.threads = threads == -1 ? TuinityConfig.tickThreads : threads; + }*/ + + public int spawnLimitMonsters; + public int spawnLimitAnimals; + public int spawnLimitWaterAmbient; + public int spawnLimitWaterAnimals; + public int spawnLimitAmbient; + + private void perWorldSpawnLimit() { + final String path = "spawn-limits"; + + this.spawnLimitMonsters = this.getInt(path + ".monsters", -1); + this.spawnLimitAnimals = this.getInt(path + ".animals", -1); + this.spawnLimitWaterAmbient = this.getInt(path + ".water-ambient", -1); + this.spawnLimitWaterAnimals = this.getInt(path + ".water-animals", -1); + this.spawnLimitAmbient = this.getInt(path + ".ambient", -1); + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/tuinity/tuinity/util/CachedLists.java b/src/main/java/com/tuinity/tuinity/util/CachedLists.java new file mode 100644 index 0000000000000000000000000000000000000000..21e50c75e0bffaa5cc5faf6aa81ae7428caca731 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/CachedLists.java @@ -0,0 +1,74 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.Chunk; +import net.minecraft.server.Entity; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.util.UnsafeList; +import java.util.List; + +public class CachedLists { + + static final UnsafeList TEMP_COLLISION_LIST = new UnsafeList<>(1024); + static boolean tempCollisionListInUse; + + public static UnsafeList getTempCollisionList() { + if (!Bukkit.isPrimaryThread() || tempCollisionListInUse) { + return new UnsafeList<>(16); + } + tempCollisionListInUse = true; + return TEMP_COLLISION_LIST; + } + + public static void returnTempCollisionList(List list) { + if (list != TEMP_COLLISION_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempCollisionListInUse = false; + } + + static final UnsafeList TEMP_GET_ENTITIES_LIST = new UnsafeList<>(1024); + static boolean tempGetEntitiesListInUse; + + public static UnsafeList getTempGetEntitiesList() { + if (!Bukkit.isPrimaryThread() || tempGetEntitiesListInUse) { + return new UnsafeList<>(16); + } + tempGetEntitiesListInUse = true; + return TEMP_GET_ENTITIES_LIST; + } + + public static void returnTempGetEntitiesList(List list) { + if (list != TEMP_GET_ENTITIES_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempGetEntitiesListInUse = false; + } + + static final UnsafeList TEMP_GET_CHUNKS_LIST = new UnsafeList<>(1024); + static boolean tempGetChunksListInUse; + + public static UnsafeList getTempGetChunksList() { + if (!Bukkit.isPrimaryThread() || tempGetChunksListInUse) { + return new UnsafeList<>(); + } + tempGetChunksListInUse = true; + return TEMP_GET_CHUNKS_LIST; + } + + public static void returnTempGetChunksList(List list) { + if (list != TEMP_GET_CHUNKS_LIST) { + return; + } + ((UnsafeList)list).setSize(0); + tempGetChunksListInUse = false; + } + + public static void reset() { + TEMP_COLLISION_LIST.completeReset(); + TEMP_GET_ENTITIES_LIST.completeReset(); + TEMP_GET_CHUNKS_LIST.completeReset(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..81fe01c122529f1716a264263957500015476f5f --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java @@ -0,0 +1,128 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.server.BlockPosition; +import net.minecraft.server.ChunkCoordIntPair; +import net.minecraft.server.Entity; +import net.minecraft.server.MathHelper; +import net.minecraft.server.SectionPosition; + +public final class CoordinateUtils { + + // dx, dz are relative to the target chunk + // dx, dz in [-radius, radius] + public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) { + return (dx + radius) + (2 * radius + 1)*(dz + radius); + } + + // the chunk keys are compatible with vanilla + + public static long getChunkKey(final BlockPosition pos) { + return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final Entity entity) { + return ((long)(MathHelper.floor(entity.locZ()) >> 4) << 32) | ((MathHelper.floor(entity.locX()) >> 4) & 0xFFFFFFFFL); + } + + public static long getChunkKey(final ChunkCoordIntPair pos) { + return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); + } + + public static long getChunkKey(final SectionPosition pos) { + return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); + } + + public static long getChunkKey(final int x, final int z) { + return ((long)z << 32) | (x & 0xFFFFFFFFL); + } + + public static int getChunkX(final long chunkKey) { + return (int)chunkKey; + } + + public static int getChunkZ(final long chunkKey) { + return (int)(chunkKey >>> 32); + } + + public static int getChunkCoordinate(final double blockCoordinate) { + return MathHelper.floor(blockCoordinate) >> 4; + } + + // the section keys are compatible with vanilla's + + static final int SECTION_X_BITS = 22; + static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; + static final int SECTION_Y_BITS = 20; + static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; + static final int SECTION_Z_BITS = 22; + static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; + // format is y,z,x (in order of LSB to MSB) + static final int SECTION_Y_SHIFT = 0; + static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; + static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; + static final int SECTION_TO_BLOCK_SHIFT = 4; + + public static long getChunkSectionKey(final int x, final int y, final int z) { + return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final SectionPosition pos) { + return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final ChunkCoordIntPair pos, final int y) { + return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getChunkSectionKey(final BlockPosition pos) { + return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static long getChunkSectionKey(final Entity entity) { + return ((MathHelper.floorLong(entity.locX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((MathHelper.floorLong(entity.locY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + ((MathHelper.floorLong(entity.locZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static int getChunkSectionX(final long key) { + return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); + } + + public static int getChunkSectionY(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); + } + + public static int getChunkSectionZ(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); + } + + // the block coordinates are not necessarily compatible with vanilla's + + public static int getBlockCoordinate(final double blockCoordinate) { + return MathHelper.floor(blockCoordinate); + } + + public static long getBlockKey(final int x, final int y, final int z) { + return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); + } + + public static long getBlockKey(final BlockPosition pos) { + return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); + } + + public static long getBlockKey(final Entity entity) { + return ((long)entity.locX() & 0x7FFFFFF) | (((long)entity.locZ() & 0x7FFFFFF) << 27) | ((long)entity.locY() << 54); + } + + private CoordinateUtils() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java @@ -0,0 +1,226 @@ +package com.tuinity.tuinity.util; + +public final class IntegerUtil { + + public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; + public static final long HIGH_BIT_U64 = Long.MIN_VALUE; + + public static int ceilLog2(final int value) { + return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static long ceilLog2(final long value) { + return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final int value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final long value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int roundCeilLog2(final int value) { + // optimized variant of 1 << (32 - leading(val - 1)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) + // HIGH_BIT_32 >>> (-1 + leading(val - 1)) + return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); + } + + public static long roundCeilLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); + } + + public static int roundFloorLog2(final int value) { + // optimized variant of 1 << (31 - leading(val)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - 31 + leading(val)) + return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); + } + + public static long roundFloorLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); + } + + public static boolean isPowerOfTwo(final int n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static boolean isPowerOfTwo(final long n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static int getTrailingBit(final int n) { + return -n & n; + } + + public static long getTrailingBit(final long n) { + return -n & n; + } + + public static int trailingZeros(final int n) { + return Integer.numberOfTrailingZeros(n); + } + + public static int trailingZeros(final long n) { + return Long.numberOfTrailingZeros(n); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorMultiple(final long numbers) { + return (int)(numbers >>> 32); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorShift(final long numbers) { + return (int)numbers; + } + + // copied from hacker's delight (signed division magic value) + // http://www.hackersdelight.org/hdcodetxt/magic.c.txt + public static long getDivisorNumbers(final int d) { + final int ad = IntegerUtil.branchlessAbs(d); + + if (ad < 2) { + throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); + } + + final int two31 = 0x80000000; + final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour + + int p = 31; + + // all these variables are UNSIGNED! + int t = two31 + (d >>> 31); + int anc = t - 1 - t%ad; + int q1 = (int)((two31 & mask)/(anc & mask)); + int r1 = two31 - q1*anc; + int q2 = (int)((two31 & mask)/(ad & mask)); + int r2 = two31 - q2*ad; + int delta; + + do { + p = p + 1; + q1 = 2*q1; // Update q1 = 2**p/|nc|. + r1 = 2*r1; // Update r1 = rem(2**p, |nc|). + if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) + q1 = q1 + 1; + r1 = r1 - anc; + } + q2 = 2*q2; // Update q2 = 2**p/|d|. + r2 = 2*r2; // Update r2 = rem(2**p, |d|). + if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) + q2 = q2 + 1; + r2 = r2 - ad; + } + delta = ad - r2; + } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); + + int magicNum = q2 + 1; + if (d < 0) { + magicNum = -magicNum; + } + int shift = p - 32; + return ((long)magicNum << 32) | shift; + } + + public static int branchlessAbs(final int val) { + // -n = -1 ^ n + 1 + final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + public static long branchlessAbs(final long val) { + // -n = -1 ^ n + 1 + final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + //https://github.com/skeeto/hash-prospector for hash functions + + //score = ~590.47984224483832 + public static int hash0(int x) { + x *= 0x36935555; + x ^= x >>> 16; + return x; + } + + //score = ~310.01596637036749 + public static int hash1(int x) { + x ^= x >>> 15; + x *= 0x356aaaad; + x ^= x >>> 17; + return x; + } + + public static int hash2(int x) { + x ^= x >>> 16; + x *= 0x7feb352d; + x ^= x >>> 15; + x *= 0x846ca68b; + x ^= x >>> 16; + return x; + } + + public static int hash3(int x) { + x ^= x >>> 17; + x *= 0xed5ad4bb; + x ^= x >>> 11; + x *= 0xac4c1b51; + x ^= x >>> 15; + x *= 0x31848bab; + x ^= x >>> 14; + return x; + } + + //score = ~365.79959673201887 + public static long hash1(long x) { + x ^= x >>> 27; + x *= 0xb24924b71d2d354bL; + x ^= x >>> 28; + return x; + } + + //h2 hash + public static long hash2(long x) { + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + x *= 0xd6e8feb86659fd93L; + x ^= x >>> 32; + return x; + } + + public static long hash3(long x) { + x ^= x >>> 45; + x *= 0xc161abe5704b6c79L; + x ^= x >>> 41; + x *= 0xe3e5389aedbc90f7L; + x ^= x >>> 56; + x *= 0x1f9aba75a52db073L; + x ^= x >>> 53; + return x; + } + + private IntegerUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java new file mode 100644 index 0000000000000000000000000000000000000000..d2c7d2c7920324d7207225ed19484e804368489d --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/IntervalledCounter.java @@ -0,0 +1,100 @@ +package com.tuinity.tuinity.util; + +public final class IntervalledCounter { + + protected long[] times; + protected final long interval; + protected long minTime; + protected int sum; + protected int head; // inclusive + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { + this.times = new long[8]; + this.interval = interval; + } + + public void updateCurrentTime() { + this.updateCurrentTime(System.nanoTime()); + } + + public void updateCurrentTime(final long currentTime) { + int sum = this.sum; + int head = this.head; + final int tail = this.tail; + final long minTime = currentTime - this.interval; + + final int arrayLen = this.times.length; + + // guard against overflow by using subtraction + while (head != tail && this.times[head] - minTime < 0) { + head = (head + 1) % arrayLen; + --sum; + } + + this.sum = sum; + this.head = head; + this.minTime = minTime; + } + + public void addTime(final long currTime) { + // guard against overflow by using subtraction + if (currTime - this.minTime < 0) { + return; + } + int nextTail = (this.tail + 1) % this.times.length; + if (nextTail == this.head) { + this.resize(); + nextTail = (this.tail + 1) % this.times.length; + } + + this.times[this.tail] = currTime; + this.tail = nextTail; + } + + public void updateAndAdd(final int count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); + for (int i = 0; i < count; ++i) { + this.addTime(currTime); + } + } + + public void updateAndAdd(final int count, final long currTime) { + this.updateCurrentTime(currTime); + for (int i = 0; i < count; ++i) { + this.addTime(currTime); + } + } + + private void resize() { + final long[] oldElements = this.times; + final long[] newElements = new long[this.times.length * 2]; + this.times = newElements; + + final int head = this.head; + final int tail = this.tail; + final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head)); + this.head = 0; + this.tail = size; + + if (tail >= head) { + System.arraycopy(oldElements, head, newElements, 0, size); + } else { + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); + } + } + + // returns in units per second + public double getRate() { + return this.size() / (this.interval * 1.0e-9); + } + + public int size() { + final int head = this.head; + final int tail = this.tail; + + return tail >= head ? (tail - head) : (tail + (this.times.length - head)); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/TickThread.java b/src/main/java/com/tuinity/tuinity/util/TickThread.java new file mode 100644 index 0000000000000000000000000000000000000000..08ed243259f052165c6f75aed1d1d65a14219715 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/TickThread.java @@ -0,0 +1,41 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; + +public final class TickThread extends Thread { + + public static final boolean STRICT_THREAD_CHECKS = Boolean.getBoolean("tuinity.strict-thread-checks"); + + static { + if (STRICT_THREAD_CHECKS) { + MinecraftServer.LOGGER.warn("Strict thread checks enabled - performance may suffer"); + } + } + + public static void softEnsureTickThread(final String reason) { + if (!STRICT_THREAD_CHECKS) { + return; + } + ensureTickThread(reason); + } + + + public static void ensureTickThread(final String reason) { + if (!Bukkit.isPrimaryThread()) { + MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + + public final int id; /* We don't override getId as the spec requires that it be unique (with respect to all other threads) */ + + public TickThread(final Runnable run, final String name, final int id) { + super(run, name); + this.id = id; + } + + public static TickThread getCurrentTickThread() { + return (TickThread)Thread.currentThread(); + } +} \ No newline at end of file diff --git a/src/main/java/com/tuinity/tuinity/util/WorldUtil.java b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..141748fe4915eb46671f1d532951f14d7080818d --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java @@ -0,0 +1,48 @@ +package com.tuinity.tuinity.util; + +import net.minecraft.server.World; + +public final class WorldUtil { + + // min, max are inclusive + // TODO update these for 1.17 + + public static int getMaxSection(final World world) { + return 15; + } + + public static int getMinSection(final World world) { + return 0; + } + + public static int getMaxLightSection(final World world) { + return getMaxSection(world) + 1; + } + + public static int getMinLightSection(final World world) { + return getMinSection(world) - 1; + } + + + + public static int getTotalSections(final World world) { + return getMaxSection(world) - getMinSection(world) + 1; + } + + public static int getTotalLightSections(final World world) { + return getMaxLightSection(world) - getMinLightSection(world) + 1; + } + + public static int getMinBlockY(final World world) { + return getMinSection(world) << 4; + } + + public static int getMaxBlockY(final World world) { + return (getMaxSection(world) << 4) | 15; + } + + private WorldUtil() { + throw new RuntimeException(); + } + +} diff --git a/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java new file mode 100644 index 0000000000000000000000000000000000000000..be408aebbccbda46e8aa82ef337574137cfa0096 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/maplist/IteratorSafeOrderedReferenceSet.java @@ -0,0 +1,335 @@ +package com.tuinity.tuinity.util.maplist; + +import it.unimi.dsi.fastutil.objects.Reference2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.bukkit.Bukkit; +import java.util.Arrays; +import java.util.NoSuchElementException; + +public final class IteratorSafeOrderedReferenceSet { + + public static final int ITERATOR_FLAG_SEE_ADDITIONS = 1 << 0; + + protected final Reference2IntLinkedOpenHashMap indexMap; + protected int firstInvalidIndex = -1; + + /* list impl */ + protected E[] listElements; + protected int listSize; + + protected final double maxFragFactor; + + protected int iteratorCount; + + private final boolean threadRestricted; + + public IteratorSafeOrderedReferenceSet() { + this(16, 0.75f, 16, 0.2); + } + + public IteratorSafeOrderedReferenceSet(final boolean threadRestricted) { + this(16, 0.75f, 16, 0.2, threadRestricted); + } + + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor) { + this(setCapacity, setLoadFactor, arrayCapacity, maxFragFactor, false); + } + public IteratorSafeOrderedReferenceSet(final int setCapacity, final float setLoadFactor, final int arrayCapacity, + final double maxFragFactor, final boolean threadRestricted) { + this.indexMap = new Reference2IntLinkedOpenHashMap<>(setCapacity, setLoadFactor); + this.indexMap.defaultReturnValue(-1); + this.maxFragFactor = maxFragFactor; + this.listElements = (E[])new Object[arrayCapacity]; + this.threadRestricted = threadRestricted; + } + + /* + public void check() { + int iterated = 0; + ReferenceOpenHashSet check = new ReferenceOpenHashSet<>(); + if (this.listElements != null) { + for (int i = 0; i < this.listSize; ++i) { + Object obj = this.listElements[i]; + if (obj != null) { + iterated++; + if (!check.add((E)obj)) { + throw new IllegalStateException("contains duplicate"); + } + if (!this.contains((E)obj)) { + throw new IllegalStateException("desync"); + } + } + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! Got " + iterated + ", expected " + this.size()); + } + + check.clear(); + iterated = 0; + for (final java.util.Iterator iterator = this.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { + final E element = iterator.next(); + iterated++; + if (!check.add(element)) { + throw new IllegalStateException("contains duplicate (iterator is wrong)"); + } + if (!this.contains(element)) { + throw new IllegalStateException("desync (iterator is wrong)"); + } + } + + if (iterated != this.size()) { + throw new IllegalStateException("Size is mismatched! (iterator is wrong) Got " + iterated + ", expected " + this.size()); + } + } + */ + + protected final boolean allowSafeIteration() { + return !this.threadRestricted || Bukkit.isPrimaryThread(); + } + + protected final double getFragFactor() { + return 1.0 - ((double)this.indexMap.size() / (double)this.listSize); + } + + public int createRawIterator() { + if (this.allowSafeIteration()) { + ++this.iteratorCount; + } + if (this.indexMap.isEmpty()) { + return -1; + } else { + return this.firstInvalidIndex == 0 ? this.indexMap.getInt(this.indexMap.firstKey()) : 0; + } + } + + public int advanceRawIterator(final int index) { + final E[] elements = this.listElements; + int ret = index + 1; + for (int len = this.listSize; ret < len; ++ret) { + if (elements[ret] != null) { + return ret; + } + } + + return -1; + } + + public void finishRawIterator() { + if (this.allowSafeIteration() && --this.iteratorCount == 0) { + if (this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + } + } + + public boolean remove(final E element) { + final int index = this.indexMap.removeInt(element); + if (index >= 0) { + if (this.firstInvalidIndex < 0 || index < this.firstInvalidIndex) { + this.firstInvalidIndex = index; + } + if (this.listElements[index] != element) { + throw new IllegalStateException(); + } + this.listElements[index] = null; + if (this.allowSafeIteration() && this.iteratorCount == 0 && this.getFragFactor() >= this.maxFragFactor) { + this.defrag(); + } + //this.check(); + return true; + } + return false; + } + + public boolean contains(final E element) { + return this.indexMap.containsKey(element); + } + + public boolean add(final E element) { + final int listSize = this.listSize; + + final int previous = this.indexMap.putIfAbsent(element, listSize); + if (previous != -1) { + return false; + } + + if (listSize >= this.listElements.length) { + this.listElements = Arrays.copyOf(this.listElements, listSize * 2); + } + this.listElements[listSize] = element; + this.listSize = listSize + 1; + + //this.check(); + return true; + } + + protected void defrag() { + if (this.firstInvalidIndex < 0) { + return; // nothing to do + } + + if (this.indexMap.isEmpty()) { + Arrays.fill(this.listElements, 0, this.listSize, null); + this.listSize = 0; + this.firstInvalidIndex = -1; + //this.check(); + return; + } + + final E[] backingArray = this.listElements; + + int lastValidIndex; + java.util.Iterator> iterator; + + if (this.firstInvalidIndex == 0) { + iterator = this.indexMap.reference2IntEntrySet().fastIterator(); + lastValidIndex = 0; + } else { + lastValidIndex = this.firstInvalidIndex; + final E key = backingArray[lastValidIndex - 1]; + iterator = this.indexMap.reference2IntEntrySet().fastIterator(new Reference2IntMap.Entry() { + @Override + public int getIntValue() { + throw new UnsupportedOperationException(); + } + + @Override + public int setValue(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public E getKey() { + return key; + } + }); + } + + while (iterator.hasNext()) { + final Reference2IntMap.Entry entry = iterator.next(); + + final int newIndex = lastValidIndex++; + backingArray[newIndex] = entry.getKey(); + entry.setValue(newIndex); + } + + // cleanup end + Arrays.fill(backingArray, lastValidIndex, this.listSize, null); + this.listSize = lastValidIndex; + this.firstInvalidIndex = -1; + //this.check(); + } + + public E rawGet(final int index) { + return this.listElements[index]; + } + + public int size() { + // always returns the correct amount - listSize can be different + return this.indexMap.size(); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator() { + return this.iterator(0); + } + + public IteratorSafeOrderedReferenceSet.Iterator iterator(final int flags) { + if (this.allowSafeIteration()) { + ++this.iteratorCount; + } + return new BaseIterator<>(this, true, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public java.util.Iterator unsafeIterator() { + return this.unsafeIterator(0); + } + public java.util.Iterator unsafeIterator(final int flags) { + return new BaseIterator<>(this, false, (flags & ITERATOR_FLAG_SEE_ADDITIONS) != 0 ? Integer.MAX_VALUE : this.listSize); + } + + public static interface Iterator extends java.util.Iterator { + + public void finishedIterating(); + + } + + protected static final class BaseIterator implements IteratorSafeOrderedReferenceSet.Iterator { + + protected final IteratorSafeOrderedReferenceSet set; + protected final boolean canFinish; + protected final int maxIndex; + protected int nextIndex; + protected E pendingValue; + protected boolean finished; + protected E lastReturned; + + protected BaseIterator(final IteratorSafeOrderedReferenceSet set, final boolean canFinish, final int maxIndex) { + this.set = set; + this.canFinish = canFinish; + this.maxIndex = maxIndex; + } + + @Override + public boolean hasNext() { + if (this.finished) { + return false; + } + if (this.pendingValue != null) { + return true; + } + + final E[] elements = this.set.listElements; + int index, len; + for (index = this.nextIndex, len = Math.min(this.maxIndex, this.set.listSize); index < len; ++index) { + final E element = elements[index]; + if (element != null) { + this.pendingValue = element; + this.nextIndex = index + 1; + return true; + } + } + + this.nextIndex = index; + return false; + } + + @Override + public E next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + final E ret = this.pendingValue; + + this.pendingValue = null; + this.lastReturned = ret; + + return ret; + } + + @Override + public void remove() { + final E lastReturned = this.lastReturned; + if (lastReturned == null) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.set.remove(lastReturned); + } + + @Override + public void finishedIterating() { + if (this.finished || !this.canFinish) { + throw new IllegalStateException(); + } + this.lastReturned = null; + this.finished = true; + if (this.set.allowSafeIteration()) { + this.set.finishRawIterator(); + } + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java new file mode 100644 index 0000000000000000000000000000000000000000..155d10994f2d7df9ac927d955d99016fe304360f --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed26WayDistancePropagator3D.java @@ -0,0 +1,295 @@ +package com.tuinity.tuinity.util.misc; + +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import net.minecraft.server.MCUtil; + +public final class Delayed26WayDistancePropagator3D { + + // this map is considered "stale" unless updates are propagated. + protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f); + + // this map is never stale + protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); + + // Generally updates to positions are made close to other updates, so we link to decrease cache misses when + // propagating updates + protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); + + @FunctionalInterface + public static interface LevelChangeCallback { + + /** + * This can be called for intermediate updates. So do not rely on newLevel being close to or + * the exact level that is expected after a full propagation has occured. + */ + public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); + + } + + protected final LevelChangeCallback changeCallback; + + public Delayed26WayDistancePropagator3D() { + this(null); + } + + public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) { + this.changeCallback = changeCallback; + } + + public int getLevel(final long pos) { + return this.levels.get(pos); + } + + public int getLevel(final int x, final int y, final int z) { + return this.levels.get(MCUtil.getSectionKey(x, y, z)); + } + + public void setSource(final int x, final int y, final int z, final int level) { + this.setSource(MCUtil.getSectionKey(x, y, z), level); + } + + public void setSource(final long coordinate, final int level) { + if ((level & 63) != level || level == 0) { + throw new IllegalArgumentException("Level must be in (0, 63], not " + level); + } + + final byte byteLevel = (byte)level; + final byte oldLevel = this.sources.put(coordinate, byteLevel); + + if (oldLevel == byteLevel) { + return; // nothing to do + } + + // queue to update later + this.updatedSources.add(coordinate); + } + + public void removeSource(final int x, final int y, final int z) { + this.removeSource(MCUtil.getSectionKey(x, y, z)); + } + + public void removeSource(final long coordinate) { + if (this.sources.remove(coordinate) != 0) { + this.updatedSources.add(coordinate); + } + } + + // queues used for BFS propagating levels + protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; + { + for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { + this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); + } + } + protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; + { + for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { + this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); + } + } + protected long levelIncreaseWorkQueueBitset; + protected long levelRemoveWorkQueueBitset; + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << level); + } + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << index); + } + + protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelRemoveWorkQueueBitset |= (1L << level); + } + + public void propagateUpdates() { + if (this.updatedSources.isEmpty()) { + return; + } + + for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final byte currentLevel = this.levels.get(coordinate); + final byte updatedSource = this.sources.get(coordinate); + + if (currentLevel == updatedSource) { + continue; + } + + if (updatedSource > currentLevel) { + // level increase + this.addToIncreaseWorkQueue(coordinate, updatedSource); + } else { + // level decrease + this.addToRemoveWorkQueue(coordinate, currentLevel); + // if the current coordinate is a source, then the decrease propagation will detect that and queue + // the source propagation + } + } + + this.updatedSources.clear(); + + // propagate source level increases first for performance reasons (in crowded areas hopefully the additions + // make the removes remove less) + this.propagateIncreases(); + + // now we propagate the decreases (which will then re-propagate clobbered sources) + this.propagateDecreases(); + } + + protected void propagateIncreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); + this.levelIncreaseWorkQueueBitset != 0L; + this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { + + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + byte level = queue.queuedLevels.removeFirstByte(); + + final boolean neighbourCheck = level < 0; + + final byte currentLevel; + if (neighbourCheck) { + level = (byte)-level; + currentLevel = this.levels.get(coordinate); + } else { + currentLevel = this.levels.putIfGreater(coordinate, level); + } + + if (neighbourCheck) { + // used when propagating from decrease to indicate that this level needs to check its neighbours + // this means the level at coordinate could be equal, but would still need neighbours checked + + if (currentLevel != level) { + // something caused the level to change, which means something propagated to it (which means + // us propagating here is redundant), or something removed the level (which means we + // cannot propagate further) + continue; + } + } else if (currentLevel >= level) { + // something higher/equal propagated + continue; + } + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); + } + + if (level == 1) { + // can't propagate 0 to neighbours + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = MCUtil.getSectionX(coordinate); + final int y = MCUtil.getSectionY(coordinate); + final int z = MCUtil.getSectionZ(coordinate); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dy | dz | dx) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = MCUtil.getSectionKey(dx + x, dy + y, dz + z); + this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + } + + protected void propagateDecreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); + this.levelRemoveWorkQueueBitset != 0L; + this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { + + final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + final byte level = queue.queuedLevels.removeFirstByte(); + + final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); + if (currentLevel == 0) { + // something else removed + continue; + } + + if (currentLevel > level) { + // something higher propagated here or we hit the propagation of another source + // in the second case we need to re-propagate because we could have just clobbered another source's + // propagation + this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking + continue; + } + + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); + } + + final byte source = this.sources.get(coordinate); + if (source != 0) { + // must re-propagate source later + this.addToIncreaseWorkQueue(coordinate, source); + } + + if (level == 0) { + // can't propagate -1 to neighbours + // we have to check neighbours for removing 1 just in case the neighbour is 2 + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = MCUtil.getSectionX(coordinate); + final int y = MCUtil.getSectionY(coordinate); + final int z = MCUtil.getSectionZ(coordinate); + + for (int dy = -1; dy <= 1; ++dy) { + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + if ((dy | dz | dx) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = MCUtil.getSectionKey(dx + x, dy + y, dz + z); + this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + + // propagate sources we clobbered in the process + this.propagateIncreases(); + } +} diff --git a/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java new file mode 100644 index 0000000000000000000000000000000000000000..606417a8aeaca2682595f417bba8e9d411999da9 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/util/misc/Delayed8WayDistancePropagator2D.java @@ -0,0 +1,713 @@ +package com.tuinity.tuinity.util.misc; + +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; +import net.minecraft.server.MCUtil; + +public final class Delayed8WayDistancePropagator2D { + + // Test + /* + protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference, Delayed8WayDistancePropagator2D test) { + int got = test.getLevel(x, z); + + int expect = 0; + Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet(); + if (nearest != null) { + for (Object _obj : nearest) { + if (_obj instanceof Ticket) { + Ticket ticket = (Ticket)_obj; + long ticketCoord = reference.getLastCoordinate(ticket); + int viewDistance = reference.getLastViewDistance(ticket); + int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x), + com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z)); + int level = viewDistance - distance; + if (level > expect) { + expect = level; + } + } + } + } + + if (expect != got) { + throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got); + } + } + + static class Ticket { + + int x; + int z; + + final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet empty + = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); + + } + + public static void main(final String[] args) { + com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap() { + @Override + protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getEmptySetFor(Ticket object) { + return object.empty; + } + }; + Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D(); + + final int maxDistance = 64; + // test origin + { + Ticket originTicket = new Ticket(); + int originDistance = 31; + // test single source + reference.add(originTicket, 0, 0, originDistance); + test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + // test single source decrease + reference.update(originTicket, 0, 0, originDistance/2); + test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + // test source increase + originDistance = 2*originDistance; + reference.update(originTicket, 0, 0, originDistance); + test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { + for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + reference.remove(originTicket); + test.removeSource(0, 0); test.propagateUpdates(); + } + + // test multiple sources at origin + { + int originDistance = 31; + java.util.List list = new java.util.ArrayList<>(); + for (int i = 0; i < 10; ++i) { + Ticket a = new Ticket(); + list.add(a); + a.x = (i & 1) == 1 ? -i : i; + a.z = (i & 1) == 1 ? -i : i; + } + for (Ticket ticket : list) { + reference.add(ticket, ticket.x, ticket.z, originDistance); + test.setSource(ticket.x, ticket.z, originDistance); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level decrease + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance/2); + test.setSource(ticket.x, ticket.z, originDistance/2); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level increase + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance*2); + test.setSource(ticket.x, ticket.z, originDistance*2); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket remove + for (int i = 0, len = list.size(); i < len; ++i) { + if ((i & 3) != 0) { + continue; + } + Ticket ticket = list.get(i); + reference.remove(ticket); + test.removeSource(ticket.x, ticket.z); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + } + + // now test at coordinate offsets + // test offset + { + Ticket originTicket = new Ticket(); + int originDistance = 31; + int offX = 54432; + int offZ = -134567; + // test single source + reference.add(originTicket, offX, offZ, originDistance); + test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + // test single source decrease + reference.update(originTicket, offX, offZ, originDistance/2); + test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate + for (int dx = -originDistance; dx <= originDistance; ++dx) { + for (int dz = -originDistance; dz <= originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + // test source increase + originDistance = 2*originDistance; + reference.update(originTicket, offX, offZ, originDistance); + test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate + for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { + for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { + test(dx + offX, dz + offZ, reference, test); + } + } + + reference.remove(originTicket); + test.removeSource(offX, offZ); test.propagateUpdates(); + } + + // test multiple sources at origin + { + int originDistance = 31; + int offX = 54432; + int offZ = -134567; + java.util.List list = new java.util.ArrayList<>(); + for (int i = 0; i < 10; ++i) { + Ticket a = new Ticket(); + list.add(a); + a.x = offX + ((i & 1) == 1 ? -i : i); + a.z = offZ + ((i & 1) == 1 ? -i : i); + } + for (Ticket ticket : list) { + reference.add(ticket, ticket.x, ticket.z, originDistance); + test.setSource(ticket.x, ticket.z, originDistance); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level decrease + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance/2); + test.setSource(ticket.x, ticket.z, originDistance/2); + } + test.propagateUpdates(); + + for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { + for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket level increase + + for (Ticket ticket : list) { + reference.update(ticket, ticket.x, ticket.z, originDistance*2); + test.setSource(ticket.x, ticket.z, originDistance*2); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + + // test ticket remove + for (int i = 0, len = list.size(); i < len; ++i) { + if ((i & 3) != 0) { + continue; + } + Ticket ticket = list.get(i); + reference.remove(ticket); + test.removeSource(ticket.x, ticket.z); + } + test.propagateUpdates(); + + for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { + for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { + test(dx, dz, reference, test); + } + } + } + } + */ + + // this map is considered "stale" unless updates are propagated. + protected final LevelMap levels = new LevelMap(8192*2, 0.6f); + + // this map is never stale + protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); + + // Generally updates to positions are made close to other updates, so we link to decrease cache misses when + // propagating updates + protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); + + @FunctionalInterface + public static interface LevelChangeCallback { + + /** + * This can be called for intermediate updates. So do not rely on newLevel being close to or + * the exact level that is expected after a full propagation has occured. + */ + public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); + + } + + protected final LevelChangeCallback changeCallback; + + public Delayed8WayDistancePropagator2D() { + this(null); + } + + public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) { + this.changeCallback = changeCallback; + } + + public int getLevel(final long pos) { + return this.levels.get(pos); + } + + public int getLevel(final int x, final int z) { + return this.levels.get(MCUtil.getCoordinateKey(x, z)); + } + + public void setSource(final int x, final int z, final int level) { + this.setSource(MCUtil.getCoordinateKey(x, z), level); + } + + public void setSource(final long coordinate, final int level) { + if ((level & 63) != level || level == 0) { + throw new IllegalArgumentException("Level must be in (0, 63], not " + level); + } + + final byte byteLevel = (byte)level; + final byte oldLevel = this.sources.put(coordinate, byteLevel); + + if (oldLevel == byteLevel) { + return; // nothing to do + } + + // queue to update later + this.updatedSources.add(coordinate); + } + + public void removeSource(final int x, final int z) { + this.removeSource(MCUtil.getCoordinateKey(x, z)); + } + + public void removeSource(final long coordinate) { + if (this.sources.remove(coordinate) != 0) { + this.updatedSources.add(coordinate); + } + } + + // queues used for BFS propagating levels + protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64]; + { + for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { + this.levelIncreaseWorkQueues[i] = new WorkQueue(); + } + } + protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64]; + { + for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { + this.levelRemoveWorkQueues[i] = new WorkQueue(); + } + } + protected long levelIncreaseWorkQueueBitset; + protected long levelRemoveWorkQueueBitset; + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { + final WorkQueue queue = this.levelIncreaseWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << level); + } + + protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { + final WorkQueue queue = this.levelIncreaseWorkQueues[index]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelIncreaseWorkQueueBitset |= (1L << index); + } + + protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { + final WorkQueue queue = this.levelRemoveWorkQueues[level]; + queue.queuedCoordinates.enqueue(coordinate); + queue.queuedLevels.enqueue(level); + + this.levelRemoveWorkQueueBitset |= (1L << level); + } + + public void propagateUpdates() { + if (this.updatedSources.isEmpty()) { + return; + } + + for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { + final long coordinate = iterator.nextLong(); + + final byte currentLevel = this.levels.get(coordinate); + final byte updatedSource = this.sources.get(coordinate); + + if (currentLevel == updatedSource) { + continue; + } + + if (updatedSource > currentLevel) { + // level increase + this.addToIncreaseWorkQueue(coordinate, updatedSource); + } else { + // level decrease + this.addToRemoveWorkQueue(coordinate, currentLevel); + // if the current coordinate is a source, then the decrease propagation will detect that and queue + // the source propagation + } + } + + this.updatedSources.clear(); + + // propagate source level increases first for performance reasons (in crowded areas hopefully the additions + // make the removes remove less) + this.propagateIncreases(); + + // now we propagate the decreases (which will then re-propagate clobbered sources) + this.propagateDecreases(); + } + + protected void propagateIncreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); + this.levelIncreaseWorkQueueBitset != 0L; + this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { + + final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + byte level = queue.queuedLevels.removeFirstByte(); + + final boolean neighbourCheck = level < 0; + + final byte currentLevel; + if (neighbourCheck) { + level = (byte)-level; + currentLevel = this.levels.get(coordinate); + } else { + currentLevel = this.levels.putIfGreater(coordinate, level); + } + + if (neighbourCheck) { + // used when propagating from decrease to indicate that this level needs to check its neighbours + // this means the level at coordinate could be equal, but would still need neighbours checked + + if (currentLevel != level) { + // something caused the level to change, which means something propagated to it (which means + // us propagating here is redundant), or something removed the level (which means we + // cannot propagate further) + continue; + } + } else if (currentLevel >= level) { + // something higher/equal propagated + continue; + } + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); + } + + if (level == 1) { + // can't propagate 0 to neighbours + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = (int)coordinate; + final int z = (int)(coordinate >>> 32); + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + if ((dx | dz) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); + this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + } + + protected void propagateDecreases() { + for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); + this.levelRemoveWorkQueueBitset != 0L; + this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { + + final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; + while (!queue.queuedLevels.isEmpty()) { + final long coordinate = queue.queuedCoordinates.removeFirstLong(); + final byte level = queue.queuedLevels.removeFirstByte(); + + final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); + if (currentLevel == 0) { + // something else removed + continue; + } + + if (currentLevel > level) { + // something higher propagated here or we hit the propagation of another source + // in the second case we need to re-propagate because we could have just clobbered another source's + // propagation + this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking + continue; + } + + if (this.changeCallback != null) { + this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); + } + + final byte source = this.sources.get(coordinate); + if (source != 0) { + // must re-propagate source later + this.addToIncreaseWorkQueue(coordinate, source); + } + + if (level == 0) { + // can't propagate -1 to neighbours + // we have to check neighbours for removing 1 just in case the neighbour is 2 + continue; + } + + // propagate to neighbours + final byte neighbourLevel = (byte)(level - 1); + final int x = (int)coordinate; + final int z = (int)(coordinate >>> 32); + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + if ((dx | dz) == 0) { + // already propagated to coordinate + continue; + } + + // sure we can check the neighbour level in the map right now and avoid a propagation, + // but then we would still have to recheck it when popping the value off of the queue! + // so just avoid the double lookup + final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); + this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); + } + } + } + } + + // propagate sources we clobbered in the process + this.propagateIncreases(); + } + + protected static final class LevelMap extends Long2ByteOpenHashMap { + public LevelMap() { + super(); + } + + public LevelMap(final int expected, final float loadFactor) { + super(expected, loadFactor); + } + + // copied from superclass + private int find(final long k) { + if (k == 0L) { + return this.containsNullKey ? this.n : -(this.n + 1); + } else { + final long[] key = this.key; + long curr; + int pos; + if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) { + return -(pos + 1); + } else if (k == curr) { + return pos; + } else { + while((curr = key[pos = pos + 1 & this.mask]) != 0L) { + if (k == curr) { + return pos; + } + } + + return -(pos + 1); + } + } + } + + // copied from superclass + private void insert(final int pos, final long k, final byte v) { + if (pos == this.n) { + this.containsNullKey = true; + } + + this.key[pos] = k; + this.value[pos] = v; + if (this.size++ >= this.maxFill) { + this.rehash(HashCommon.arraySize(this.size + 1, this.f)); + } + } + + // copied from superclass + public byte putIfGreater(final long key, final byte value) { + final int pos = this.find(key); + if (pos < 0) { + if (this.defRetValue < value) { + this.insert(-pos - 1, key, value); + } + return this.defRetValue; + } else { + final byte curr = this.value[pos]; + if (value > curr) { + this.value[pos] = value; + return curr; + } + return curr; + } + } + + // copied from superclass + private void removeEntry(final int pos) { + --this.size; + this.shiftKeys(pos); + if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { + this.rehash(this.n / 2); + } + } + + // copied from superclass + private void removeNullEntry() { + this.containsNullKey = false; + --this.size; + if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { + this.rehash(this.n / 2); + } + } + + // copied from superclass + public byte removeIfGreaterOrEqual(final long key, final byte value) { + if (key == 0L) { + if (!this.containsNullKey) { + return this.defRetValue; + } + final byte current = this.value[this.n]; + if (value >= current) { + this.removeNullEntry(); + return current; + } + return current; + } else { + long[] keys = this.key; + byte[] values = this.value; + long curr; + int pos; + if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) { + return this.defRetValue; + } else if (key == curr) { + final byte current = values[pos]; + if (value >= current) { + this.removeEntry(pos); + return current; + } + return current; + } else { + while((curr = keys[pos = pos + 1 & this.mask]) != 0L) { + if (key == curr) { + final byte current = values[pos]; + if (value >= current) { + this.removeEntry(pos); + return current; + } + return current; + } + } + + return this.defRetValue; + } + } + } + } + + protected static final class WorkQueue { + + public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque(); + public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque(); + + } + + protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue { + + /** + * Assumes non-empty. If empty, undefined behaviour. + */ + public long removeFirstLong() { + // copied from superclass + long t = this.array[this.start]; + if (++this.start == this.length) { + this.start = 0; + } + + return t; + } + } + + protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue { + + /** + * Assumes non-empty. If empty, undefined behaviour. + */ + public byte removeFirstByte() { + // copied from superclass + byte t = this.array[this.start]; + if (++this.start == this.length) { + this.start = 0; + } + + return t; + } + } +} diff --git a/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java new file mode 100644 index 0000000000000000000000000000000000000000..002abb3cbf0f742e685f2f043d2600de03e37a19 --- /dev/null +++ b/src/main/java/com/tuinity/tuinity/voxel/AABBVoxelShape.java @@ -0,0 +1,165 @@ +package com.tuinity.tuinity.voxel; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.server.AxisAlignedBB; +import net.minecraft.server.EnumDirection; +import net.minecraft.server.VoxelShape; +import net.minecraft.server.VoxelShapes; +import java.util.ArrayList; +import java.util.List; + +public final class AABBVoxelShape extends VoxelShape { + + public final AxisAlignedBB aabb; + + public AABBVoxelShape(AxisAlignedBB aabb) { + super(VoxelShapes.getFullUnoptimisedCube().getShape()); + this.aabb = aabb; + } + + @Override + public boolean isEmpty() { + return this.aabb.isEmpty(); + } + + @Override + public double b(EnumDirection.EnumAxis enumdirection_enumaxis) { // getMin + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return this.aabb.minX; + case 1: + return this.aabb.minY; + case 2: + return this.aabb.minZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public double c(EnumDirection.EnumAxis enumdirection_enumaxis) { //getMax + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return this.aabb.maxX; + case 1: + return this.aabb.maxY; + case 2: + return this.aabb.maxZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public AxisAlignedBB getBoundingBox() { // rets bounding box enclosing this entire shape + return this.aabb; + } + + // enum direction axis is from 0 -> 2, so we keep the lower bits for direction axis. + @Override + protected double a(EnumDirection.EnumAxis enumdirection_enumaxis, int i) { // getPointFromIndex + switch (enumdirection_enumaxis.ordinal() | (i << 2)) { + case (0 | (0 << 2)): + return this.aabb.minX; + case (1 | (0 << 2)): + return this.aabb.minY; + case (2 | (0 << 2)): + return this.aabb.minZ; + case (0 | (1 << 2)): + return this.aabb.maxX; + case (1 | (1 << 2)): + return this.aabb.maxY; + case (2 | (1 << 2)): + return this.aabb.maxZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + private DoubleList cachedListX; + private DoubleList cachedListY; + private DoubleList cachedListZ; + + @Override + protected DoubleList a(EnumDirection.EnumAxis enumdirection_enumaxis) { // getPoints + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return this.cachedListX == null ? this.cachedListX = DoubleArrayList.wrap(new double[] { this.aabb.minX, this.aabb.maxX }) : this.cachedListX; + case 1: + return this.cachedListY == null ? this.cachedListY = DoubleArrayList.wrap(new double[] { this.aabb.minY, this.aabb.maxY }) : this.cachedListY; + case 2: + return this.cachedListZ == null ? this.cachedListZ = DoubleArrayList.wrap(new double[] { this.aabb.minZ, this.aabb.maxZ }) : this.cachedListZ; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public VoxelShape a(double d0, double d1, double d2) { // createOffset + return new AABBVoxelShape(this.aabb.offset(d0, d1, d2)); + } + + @Override + public VoxelShape c() { // simplify + return this; + } + + @Override + public void b(VoxelShapes.a voxelshapes_a) { // forEachAABB + voxelshapes_a.consume(this.aabb.minX, this.aabb.minY, this.aabb.minZ, this.aabb.maxX, this.aabb.maxY, this.aabb.maxZ); + } + + @Override + public List d() { // getAABBs + List ret = new ArrayList<>(1); + ret.add(this.aabb); + return ret; + } + + @Override + protected int a(EnumDirection.EnumAxis enumdirection_enumaxis, double d0) { // findPointIndexAfterOffset + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return d0 < this.aabb.maxX ? (d0 < this.aabb.minX ? -1 : 0) : 1; + case 1: + return d0 < this.aabb.maxY ? (d0 < this.aabb.minY ? -1 : 0) : 1; + case 2: + return d0 < this.aabb.maxZ ? (d0 < this.aabb.minZ ? -1 : 0) : 1; + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + protected boolean b(double d0, double d1, double d2) { // containsPoint + return this.aabb.contains(d0, d1, d2); + } + + @Override + public VoxelShape a(EnumDirection enumdirection) { // unknown + return super.a(enumdirection); + } + + @Override + public double a(EnumDirection.EnumAxis enumdirection_enumaxis, AxisAlignedBB axisalignedbb, double d0) { // collide + if (this.aabb.isEmpty() || axisalignedbb.isEmpty()) { + return d0; + } + switch (enumdirection_enumaxis.ordinal()) { + case 0: + return AxisAlignedBB.collideX(this.aabb, axisalignedbb, d0); + case 1: + return AxisAlignedBB.collideY(this.aabb, axisalignedbb, d0); + case 2: + return AxisAlignedBB.collideZ(this.aabb, axisalignedbb, d0); + default: + throw new IllegalStateException("Unknown axis requested"); + } + } + + @Override + public boolean intersects(AxisAlignedBB axisalingedbb) { + return this.aabb.voxelShapeIntersect(axisalingedbb); + } +} diff --git a/src/main/java/net/minecraft/server/AxisAlignedBB.java b/src/main/java/net/minecraft/server/AxisAlignedBB.java index ed9b2f9adfecdc6d1b9925579ec510657adde11f..fd3bb6dfa6cc060e9785c22a9e61a4325c348e36 100644 --- a/src/main/java/net/minecraft/server/AxisAlignedBB.java +++ b/src/main/java/net/minecraft/server/AxisAlignedBB.java @@ -13,6 +13,157 @@ public class AxisAlignedBB { public final double maxY; public final double maxZ; + // Tuinity start + public final boolean isEmpty() { + return (this.maxX - this.minX) < MCUtil.COLLISION_EPSILON && (this.maxY - this.minY) < MCUtil.COLLISION_EPSILON && (this.maxZ - this.minZ) < MCUtil.COLLISION_EPSILON; + } + + public static AxisAlignedBB getBoxForChunk(int chunkX, int chunkZ) { + double x = (double)(chunkX << 4); + double z = (double)(chunkZ << 4); + // use a bounding box bigger than the chunk to prevent entities from entering it on move + return new AxisAlignedBB(x - 3*MCUtil.COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*MCUtil.COLLISION_EPSILON, x + (16.0 + 3*MCUtil.COLLISION_EPSILON), Double.POSITIVE_INFINITY, z + (16.0 + 3*MCUtil.COLLISION_EPSILON), false); + } + + /* + A couple of rules for VoxelShape collisions: + Two shapes only intersect if they are actually more than EPSILON units into each other. This also applies to movement + checks. + If the two shapes strictly collide, then the return value of a collide call will return a value in the opposite + direction of the source move. However, this value will not be greater in magnitude than EPSILON. Collision code + will automatically round it to 0. + */ + + public final boolean voxelShapeIntersect(AxisAlignedBB other) { + return (this.minX - other.maxX) < -MCUtil.COLLISION_EPSILON && (this.maxX - other.minX) > MCUtil.COLLISION_EPSILON && + (this.minY - other.maxY) < -MCUtil.COLLISION_EPSILON && (this.maxY - other.minY) > MCUtil.COLLISION_EPSILON && + (this.minZ - other.maxZ) < -MCUtil.COLLISION_EPSILON && (this.maxZ - other.minZ) > MCUtil.COLLISION_EPSILON; + } + + public final boolean voxelShapeIntersect(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return (this.minX - maxX) < -MCUtil.COLLISION_EPSILON && (this.maxX - minX) > MCUtil.COLLISION_EPSILON && + (this.minY - maxY) < -MCUtil.COLLISION_EPSILON && (this.maxY - minY) > MCUtil.COLLISION_EPSILON && + (this.minZ - maxZ) < -MCUtil.COLLISION_EPSILON && (this.maxZ - minZ) > MCUtil.COLLISION_EPSILON; + } + + public static boolean voxelShapeIntersect(double minX1, double minY1, double minZ1, double maxX1, double maxY1, double maxZ1, + double minX2, double minY2, double minZ2, double maxX2, double maxY2, double maxZ2) { + return (minX1 - maxX2) < -MCUtil.COLLISION_EPSILON && (maxX1 - minX2) > MCUtil.COLLISION_EPSILON && + (minY1 - maxY2) < -MCUtil.COLLISION_EPSILON && (maxY1 - minY2) > MCUtil.COLLISION_EPSILON && + (minZ1 - maxZ2) < -MCUtil.COLLISION_EPSILON && (maxZ1 - minZ2) > MCUtil.COLLISION_EPSILON; + } + + public static double collideX(AxisAlignedBB target, AxisAlignedBB source, double source_move) { + if (Math.abs(source_move) < MCUtil.COLLISION_EPSILON) { + return 0.0; + } + + if ((source.minY - target.maxY) < -MCUtil.COLLISION_EPSILON && (source.maxY - target.minY) > MCUtil.COLLISION_EPSILON && + (source.minZ - target.maxZ) < -MCUtil.COLLISION_EPSILON && (source.maxZ - target.minZ) > MCUtil.COLLISION_EPSILON) { + + if (source_move >= 0.0) { + double max_move = target.minX - source.maxX; // < 0.0 if no strict collision + if (max_move < -MCUtil.COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + double max_move = target.maxX - source.minX; // > 0.0 if no strict collision + if (max_move > MCUtil.COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public static double collideY(AxisAlignedBB target, AxisAlignedBB source, double source_move) { + if (Math.abs(source_move) < MCUtil.COLLISION_EPSILON) { + return 0.0; + } + + if ((source.minX - target.maxX) < -MCUtil.COLLISION_EPSILON && (source.maxX - target.minX) > MCUtil.COLLISION_EPSILON && + (source.minZ - target.maxZ) < -MCUtil.COLLISION_EPSILON && (source.maxZ - target.minZ) > MCUtil.COLLISION_EPSILON) { + if (source_move >= 0.0) { + double max_move = target.minY - source.maxY; // < 0.0 if no strict collision + if (max_move < -MCUtil.COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + double max_move = target.maxY - source.minY; // > 0.0 if no strict collision + if (max_move > MCUtil.COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public static double collideZ(AxisAlignedBB target, AxisAlignedBB source, double source_move) { + if (Math.abs(source_move) < MCUtil.COLLISION_EPSILON) { + return 0.0; + } + + if ((source.minX - target.maxX) < -MCUtil.COLLISION_EPSILON && (source.maxX - target.minX) > MCUtil.COLLISION_EPSILON && + (source.minY - target.maxY) < -MCUtil.COLLISION_EPSILON && (source.maxY - target.minY) > MCUtil.COLLISION_EPSILON) { + if (source_move >= 0.0) { + double max_move = target.minZ - source.maxZ; // < 0.0 if no strict collision + if (max_move < -MCUtil.COLLISION_EPSILON) { + return source_move; + } + return Math.min(max_move, source_move); + } else { + double max_move = target.maxZ - source.minZ; // > 0.0 if no strict collision + if (max_move > MCUtil.COLLISION_EPSILON) { + return source_move; + } + return Math.max(max_move, source_move); + } + } + return source_move; + } + + public final AxisAlignedBB offsetX(double dx) { + return new AxisAlignedBB(this.minX + dx, this.minY, this.minZ, this.maxX + dx, this.maxY, this.maxZ, false); + } + + public final AxisAlignedBB offsetY(double dy) { + return new AxisAlignedBB(this.minX, this.minY + dy, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false); + } + + public final AxisAlignedBB offsetZ(double dz) { + return new AxisAlignedBB(this.minX, this.minY, this.minZ + dz, this.maxX, this.maxY, this.maxZ + dz, false); + } + + public AxisAlignedBB(double d0, double d1, double d2, double d3, double d4, double d5, boolean dummy) { + this.minX = d0; + this.minY = d1; + this.minZ = d2; + this.maxX = d3; + this.maxY = d4; + this.maxZ = d5; + } + + public final AxisAlignedBB expandUpwards(double dy) { + return new AxisAlignedBB(this.minX, this.minY, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false); + } + + public final AxisAlignedBB cutUpwards(final double dy) { // dy > 0.0 + return new AxisAlignedBB(this.minX, this.maxY, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false); + } + + public final AxisAlignedBB cutDownwards(final double dy) { // dy < 0.0 + return new AxisAlignedBB(this.minX, this.minY + dy, this.minZ, this.maxX, this.minY, this.maxZ, false); + } + + public final AxisAlignedBB expandUpwardsAndCutBelow(double dy) { + return new AxisAlignedBB(this.minX, this.maxY, this.minZ, this.maxX, this.maxY + dy, this.maxZ, false); + } + // Tuinity end + public AxisAlignedBB(double d0, double d1, double d2, double d3, double d4, double d5) { this.minX = Math.min(d0, d3); this.minY = Math.min(d1, d4); @@ -185,6 +336,7 @@ public class AxisAlignedBB { return new AxisAlignedBB(d0, d1, d2, d3, d4, d5); } + public final AxisAlignedBB offset(double d0, double d1, double d2) { return this.d(d0, d1, d2); } // Tuinity - OBFHELPER public AxisAlignedBB d(double d0, double d1, double d2) { return new AxisAlignedBB(this.minX + d0, this.minY + d1, this.minZ + d2, this.maxX + d0, this.maxY + d1, this.maxZ + d2); } @@ -193,6 +345,7 @@ public class AxisAlignedBB { return new AxisAlignedBB(this.minX + (double) blockposition.getX(), this.minY + (double) blockposition.getY(), this.minZ + (double) blockposition.getZ(), this.maxX + (double) blockposition.getX(), this.maxY + (double) blockposition.getY(), this.maxZ + (double) blockposition.getZ()); } + public final AxisAlignedBB offset(Vec3D vec3d) { return this.b(vec3d); } // Tuinity - OBFHELPER public AxisAlignedBB c(Vec3D vec3d) { return this.d(vec3d.x, vec3d.y, vec3d.z); } @@ -212,6 +365,7 @@ public class AxisAlignedBB { return this.e(vec3d.x, vec3d.y, vec3d.z); } + public final boolean contains(double d0, double d1, double d2) { return this.e(d0, d1, d2); } // Tuinity - OBFHELPER public boolean e(double d0, double d1, double d2) { return d0 >= this.minX && d0 < this.maxX && d1 >= this.minY && d1 < this.maxY && d2 >= this.minZ && d2 < this.maxZ; } diff --git a/src/main/java/net/minecraft/server/BaseBlockPosition.java b/src/main/java/net/minecraft/server/BaseBlockPosition.java index 6b655b744d31d9660c7521ab596b27bcd92f4d58..e811295b4d6afcd920f60e0ce5440e43300d9085 100644 --- a/src/main/java/net/minecraft/server/BaseBlockPosition.java +++ b/src/main/java/net/minecraft/server/BaseBlockPosition.java @@ -16,9 +16,9 @@ public class BaseBlockPosition implements Comparable { return IntStream.of(new int[]{baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ()}); }); public static final BaseBlockPosition ZERO = new BaseBlockPosition(0, 0, 0); - private int a;public final void setX(final int x) { this.a = x; } // Paper - OBFHELPER - private int b;public final void setY(final int y) { this.b = y; } // Paper - OBFHELPER - private int e;public final void setZ(final int z) { this.e = z; } // Paper - OBFHELPER + protected int a; // Paper - OBFHELPER // Tuinity - private->protected - diff on change, this is the x coordinate - Also revert the decision to expose set on an immutable type + protected int b; // Paper - OBFHELPER // Tuinity - private->protected - diff on change, this is the y coordinate - Also revert the decision to expose set on an immutable type + protected int e; // Paper - OBFHELPER // Tuinity - private->protected - diff on change, this is the z coordinate - Also revert the decision to expose set on an immutable type // Paper start public boolean isValidLocation() { @@ -71,15 +71,15 @@ public class BaseBlockPosition implements Comparable { return this.e; } - public void o(int i) { // Paper - protected -> public + protected void o_unused(int i) { // Paper - protected -> public // Tuinity - not needed here - Also revert the decision to expose set on an immutable type this.a = i; } - public void p(int i) { // Paper - protected -> public + protected void p_unused(int i) { // Paper - protected -> public // Tuinity - not needed here - Also revert the decision to expose set on an immutable type this.b = i; } - public void q(int i) { // Paper - protected -> public + protected void q_unused(int i) { // Paper - protected -> public // Tuinity - not needed here - Also revert the decision to expose set on an immutable type this.e = i; } diff --git a/src/main/java/net/minecraft/server/Behavior.java b/src/main/java/net/minecraft/server/Behavior.java index 65af976527133ee5c2f52e411e19c4f7f06df3ef..0b9d469a92decfb0632805791868ef7faa88c535 100644 --- a/src/main/java/net/minecraft/server/Behavior.java +++ b/src/main/java/net/minecraft/server/Behavior.java @@ -7,7 +7,7 @@ import java.util.Map.Entry; public abstract class Behavior { protected final Map, MemoryStatus> a; - private Behavior.Status b; + private Behavior.Status b; public final Behavior.Status getStatus() { return this.b; } // Tuinity - OBFHELPER private long c; private final int d; private final int e; diff --git a/src/main/java/net/minecraft/server/BehaviorFindPosition.java b/src/main/java/net/minecraft/server/BehaviorFindPosition.java index 63a761ebef80d4af09cdc2682e496d78492c4a3a..8d445e9c0875db6cf45e4d8bcfce7cd3d5094d94 100644 --- a/src/main/java/net/minecraft/server/BehaviorFindPosition.java +++ b/src/main/java/net/minecraft/server/BehaviorFindPosition.java @@ -55,6 +55,227 @@ public class BehaviorFindPosition extends Behavior { } } + // Tuinity - remove streams entirely for poi search + // the only intentional vanilla diff is that this function will NOT load in poi data, anything else is a bug! + protected static Set findNearestPoi(VillagePlace poiStorage, + Predicate villagePlaceType, + Predicate positionPredicate, + BlockPosition sourcePosition, + int range, // distance on x y z axis + VillagePlace.Occupancy occupancy, + int max) { + java.util.TreeSet ret = new java.util.TreeSet<>((blockpos1, blockpos2) -> { + // important to keep distanceSquared order: the param is the source + return Double.compare(blockpos1.distanceSquared(sourcePosition), blockpos2.distanceSquared(sourcePosition)); + }); + findNearestPoi(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, max, ret); + return new java.util.HashSet<>(ret); + } + protected static void findNearestPoi(VillagePlace poiStorage, + Predicate villagePlaceType, + Predicate positionPredicate, + BlockPosition sourcePosition, + int range, // distance on x y z axis + VillagePlace.Occupancy occupancy, + int max, + java.util.SortedSet ret) { + // the biggest issue with the original mojang implementation is that they chain so many streams together + // the amount of streams chained just rolls performance, even if nothing is iterated over + Predicate occupancyFilter = occupancy.getPredicate(); + double rangeSquared = range * range; + + // First up, we need to iterate the chunks + // all the values here are in chunk sections + int lowerX = MathHelper.floor(sourcePosition.getX() - range) >> 4; + int lowerY = Math.max(0, MathHelper.floor(sourcePosition.getY() - range) >> 4); + int lowerZ = MathHelper.floor(sourcePosition.getZ() - range) >> 4; + int upperX = MathHelper.floor(sourcePosition.getX() + range) >> 4; + int upperY = Math.min(15, MathHelper.floor(sourcePosition.getY() + range) >> 4); + int upperZ = MathHelper.floor(sourcePosition.getZ() + range) >> 4; + + // Vanilla iterates by x until max is reached then increases z + // vanilla also searches by increasing Y section value + for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { + for (int currX = lowerX; currX <= upperX; ++currX) { + for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need + Optional poiSectionOptional = poiStorage.getIfLoaded(SectionPosition.asLong(currX, currY, currZ)); + VillagePlaceSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); + if (poiSection == null) { + continue; + } + + java.util.Map> sectionData = poiSection.getData(); + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (java.util.Iterator>> iterator = sectionData.entrySet().iterator(); + iterator.hasNext();) { + java.util.Map.Entry> entry = iterator.next(); + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (VillagePlaceRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + // vanilla code is pretty dumb about filtering by distance: first they filter out + // so that only values in the square radius of range are returned but then they + // filter out so that the distance is in range + // but there's a catch! distanceSquared, by default, will ADD 0.5 to ONLY ONE OF the + // block position parameters (itself, in this case the poi position)! So if we want to + // maintain exact vanilla behaviour, well shit we need to play dumb as well. + + BlockPosition poiPosition = poiData.getPosition(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + if (poiPosition.distanceSquared(sourcePosition) > rangeSquared) { + // out of range for distance check + continue; + } + + if (!positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + // found one! + ret.add(poiPosition); + if (ret.size() > max) { + ret.remove(ret.last()); + } + } + } + } + } + } + } + + protected static BlockPosition findAnyFirstPoi(VillagePlace poiStorage, + Predicate villagePlaceType, + Predicate positionPredicate, + BlockPosition sourcePosition, + int range, // distance on x y z axis + VillagePlace.Occupancy occupancy) { + Set ret = new java.util.HashSet<>(); + findPoi(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, 1, ret); + return ret.isEmpty() ? null : ret.iterator().next(); + } + + protected static Set findPoi(VillagePlace poiStorage, + Predicate villagePlaceType, + Predicate positionPredicate, + BlockPosition sourcePosition, + int range, // distance on x y z axis + VillagePlace.Occupancy occupancy, + int max) { + Set ret = new java.util.HashSet<>(); + findPoi(poiStorage, villagePlaceType, positionPredicate, sourcePosition, range, occupancy, max, ret); + return ret; + } + protected static void findPoi(VillagePlace poiStorage, + Predicate villagePlaceType, + Predicate positionPredicate, + BlockPosition sourcePosition, + int range, // distance on x y z axis + VillagePlace.Occupancy occupancy, + int max, + Set ret) { + // the biggest issue with the original mojang implementation is that they chain so many streams together + // the amount of streams chained just rolls performance, even if nothing is iterated over + Predicate occupancyFilter = occupancy.getPredicate(); + double rangeSquared = range * range; + + // First up, we need to iterate the chunks + // all the values here are in chunk sections + int lowerX = MathHelper.floor(sourcePosition.getX() - range) >> 4; + int lowerY = Math.max(0, MathHelper.floor(sourcePosition.getY() - range) >> 4); + int lowerZ = MathHelper.floor(sourcePosition.getZ() - range) >> 4; + int upperX = MathHelper.floor(sourcePosition.getX() + range) >> 4; + int upperY = Math.min(15, MathHelper.floor(sourcePosition.getY() + range) >> 4); + int upperZ = MathHelper.floor(sourcePosition.getZ() + range) >> 4; + + // Vanilla iterates by x until max is reached then increases z + // vanilla also searches by increasing Y section value + for (int currZ = lowerZ; currZ <= upperZ; ++currZ) { + for (int currX = lowerX; currX <= upperX; ++currX) { + for (int currY = lowerY; currY <= upperY; ++currY) { // vanilla searches the entire chunk because they're actually stupid. just search the sections we need + Optional poiSectionOptional = poiStorage.getIfLoaded(SectionPosition.asLong(currX, currY, currZ)); + VillagePlaceSection poiSection = poiSectionOptional == null ? null : poiSectionOptional.orElse(null); + if (poiSection == null) { + continue; + } + + java.util.Map> sectionData = poiSection.getData(); + if (sectionData.isEmpty()) { + continue; + } + + // now we search the section data + for (java.util.Iterator>> iterator = sectionData.entrySet().iterator(); + iterator.hasNext();) { + java.util.Map.Entry> entry = iterator.next(); + if (!villagePlaceType.test(entry.getKey())) { + // filter out by poi type + continue; + } + + // now we can look at the poi data + for (VillagePlaceRecord poiData : entry.getValue()) { + if (!occupancyFilter.test(poiData)) { + // filter by occupancy + continue; + } + + // vanilla code is pretty dumb about filtering by distance: first they filter out + // so that only values in the square radius of range are returned but then they + // filter out so that the distance is in range + // but there's a catch! distanceSquared, by default, will ADD 0.5 to ONLY ONE OF the + // block position parameters (itself, in this case the poi position)! So if we want to + // maintain exact vanilla behaviour, well shit we need to play dumb as well. + + BlockPosition poiPosition = poiData.getPosition(); + + if (Math.abs(poiPosition.getX() - sourcePosition.getX()) > range + || Math.abs(poiPosition.getZ() - sourcePosition.getZ()) > range) { + // out of range for square radius + continue; + } + + if (poiPosition.distanceSquared(sourcePosition) > rangeSquared) { + // out of range for distance check + continue; + } + + if (!positionPredicate.test(poiPosition)) { + // filter by position + continue; + } + + // found one! + ret.add(poiPosition); + if (ret.size() >= max) { + return; + } + } + } + } + } + } + } + // Tuinity - remove streams entirely for poi search + protected void a(WorldServer worldserver, EntityCreature entitycreature, long i) { this.f = i + 20L + (long) worldserver.getRandom().nextInt(20); VillagePlace villageplace = worldserver.y(); @@ -74,7 +295,7 @@ public class BehaviorFindPosition extends Behavior { return true; } }; - Set set = (Set) villageplace.b(this.b.c(), predicate, entitycreature.getChunkCoordinates(), 48, VillagePlace.Occupancy.HAS_SPACE).limit(5L).collect(Collectors.toSet()); + Set set = findNearestPoi(villageplace, this.b.c(), predicate, entitycreature.getChunkCoordinates(), 48, VillagePlace.Occupancy.HAS_SPACE, 5); // Tuinity - remove streams entirely for poi search PathEntity pathentity = entitycreature.getNavigation().a(set, this.b.d()); if (pathentity != null && pathentity.j()) { @@ -84,7 +305,7 @@ public class BehaviorFindPosition extends Behavior { villageplace.a(this.b.c(), (blockposition1) -> { return blockposition1.equals(blockposition); }, blockposition, 1); - entitycreature.getBehaviorController().setMemory(this.c, (Object) GlobalPos.create(worldserver.getDimensionKey(), blockposition)); + entitycreature.getBehaviorController().setMemory(this.c, GlobalPos.create(worldserver.getDimensionKey(), blockposition)); // Tuinity - decompile fix this.e.ifPresent((obyte) -> { worldserver.broadcastEntityEffect(entitycreature, obyte); }); diff --git a/src/main/java/net/minecraft/server/BehaviorGate.java b/src/main/java/net/minecraft/server/BehaviorGate.java index 46e910581210421c8699637431804dc2f43eb4a6..fb967bc03f58fab8cec2732b1890108f2fc66af8 100644 --- a/src/main/java/net/minecraft/server/BehaviorGate.java +++ b/src/main/java/net/minecraft/server/BehaviorGate.java @@ -12,7 +12,7 @@ public class BehaviorGate extends Behavior { private final Set> b; private final BehaviorGate.Order c; private final BehaviorGate.Execution d; - private final WeightedList> e = new WeightedList<>(false); // Paper - don't use a clone + private final WeightedList> e = new WeightedList<>(false); protected final WeightedList> getList() { return this.e; } // Paper - don't use a clone // Tuinity - OBFHELPER public BehaviorGate(Map, MemoryStatus> map, Set> set, BehaviorGate.Order behaviorgate_order, BehaviorGate.Execution behaviorgate_execution, List, Integer>> list) { super(map); @@ -26,11 +26,17 @@ public class BehaviorGate extends Behavior { @Override protected boolean b(WorldServer worldserver, E e0, long i) { - return this.e.c().filter((behavior) -> { - return behavior.a() == Behavior.Status.RUNNING; - }).anyMatch((behavior) -> { - return behavior.b(worldserver, e0, i); - }); + // Tuinity start - remove streams + List>> list = this.getList().getList(); + for (int index = 0, len = list.size(); index < len; ++index) { + Behavior behavior = list.get(index).getValue(); + if (behavior.getStatus() == Status.RUNNING && behavior.b(worldserver, e0, i)) { // copied from removed code, make sure to update + return true; + } + } + + return false; + // Tuinity end - remove streams } @Override @@ -46,20 +52,28 @@ public class BehaviorGate extends Behavior { @Override protected void d(WorldServer worldserver, E e0, long i) { - this.e.c().filter((behavior) -> { - return behavior.a() == Behavior.Status.RUNNING; - }).forEach((behavior) -> { - behavior.f(worldserver, e0, i); - }); + // Tuinity start - remove streams + List>> list = this.getList().getList(); + for (int index = 0, len = list.size(); index < len; ++index) { + Behavior behavior = list.get(index).getValue(); + if (behavior.getStatus() == Behavior.Status.RUNNING) { + behavior.f(worldserver, e0, i); // copied from removed code, make sure to update + } + } + // Tuinity end - remove streams } @Override protected void c(WorldServer worldserver, E e0, long i) { - this.e.c().filter((behavior) -> { - return behavior.a() == Behavior.Status.RUNNING; - }).forEach((behavior) -> { - behavior.g(worldserver, e0, i); - }); + // Tuinity start - remove streams + List>> list = this.getList().getList(); + for (int index = 0, len = list.size(); index < len; ++index) { + Behavior behavior = list.get(index).getValue(); + if (behavior.getStatus() == Behavior.Status.RUNNING) { + behavior.g(worldserver, e0, i); // copied from removed code, make sure to update + } + } + // Tuinity end - remove streams BehaviorController behaviorcontroller = e0.getBehaviorController(); this.b.forEach(behaviorcontroller::removeMemory); // Paper - decomp fix @@ -79,21 +93,29 @@ public class BehaviorGate extends Behavior { RUN_ONE { @Override public void a(WeightedList> weightedlist, WorldServer worldserver, E e0, long i) { - weightedlist.c().filter((behavior) -> { - return behavior.a() == Behavior.Status.STOPPED; - }).filter((behavior) -> { - return behavior.e(worldserver, e0, i); - }).findFirst(); + // Tuinity start - remove streams + List>> list = weightedlist.getList(); + for (int index = 0, len = list.size(); index < len; ++index) { + Behavior behavior = list.get(index).getValue(); + if (behavior.getStatus() == Behavior.Status.STOPPED && behavior.e(worldserver, e0, i)) { // copied from removed code, make sure to update + break; + } + } + // Tuinity end - remove streams } }, TRY_ALL { @Override public void a(WeightedList> weightedlist, WorldServer worldserver, E e0, long i) { - weightedlist.c().filter((behavior) -> { - return behavior.a() == Behavior.Status.STOPPED; - }).forEach((behavior) -> { - behavior.e(worldserver, e0, i); - }); + // Tuinity start - remove streams + List>> list = weightedlist.getList(); + for (int index = 0, len = list.size(); index < len; ++index) { + Behavior behavior = list.get(index).getValue(); + if (behavior.getStatus() == Behavior.Status.STOPPED) { + behavior.e(worldserver, e0, i); // copied from removed code, make sure to update + } + } + // Tuinity end - remove streams } }; diff --git a/src/main/java/net/minecraft/server/BehaviorLookInteract.java b/src/main/java/net/minecraft/server/BehaviorLookInteract.java index a33303c31881b6391723e16a06d7841d48679958..ce57e6a4acac97d6da82202094306e7e91f1c87e 100644 --- a/src/main/java/net/minecraft/server/BehaviorLookInteract.java +++ b/src/main/java/net/minecraft/server/BehaviorLookInteract.java @@ -7,7 +7,7 @@ import java.util.function.Predicate; public class BehaviorLookInteract extends Behavior { private final EntityTypes b; - private final int c; + private final int c; private final int getMaxRange() { return this.c; } // Tuinity - OBFHELPER private final Predicate d; private final Predicate e; @@ -29,7 +29,20 @@ public class BehaviorLookInteract extends Behavior { @Override public boolean a(WorldServer worldserver, EntityLiving entityliving) { - return this.e.test(entityliving) && this.b(entityliving).stream().anyMatch(this::a); + // Tuinity start - remove streams + if (!this.e.test(entityliving)) { + return false; + } + + List list = this.b(entityliving); + for (int index = 0, len = list.size(); index < len; ++index) { + if (this.a(list.get(index))) { + return true; + } + } + + return false; + // Tuinity end - remove streams } @Override @@ -37,16 +50,28 @@ public class BehaviorLookInteract extends Behavior { super.a(worldserver, entityliving, i); BehaviorController behaviorcontroller = entityliving.getBehaviorController(); - behaviorcontroller.getMemory(MemoryModuleType.VISIBLE_MOBS).ifPresent((list) -> { - list.stream().filter((entityliving1) -> { - return entityliving1.h((Entity) entityliving) <= (double) this.c; - }).filter(this::a).findFirst().ifPresent((entityliving1) -> { - behaviorcontroller.setMemory(MemoryModuleType.INTERACTION_TARGET, (Object) entityliving1); - behaviorcontroller.setMemory(MemoryModuleType.LOOK_TARGET, (Object) (new BehaviorPositionEntity(entityliving1, true))); - }); - }); + // Tuinity start - remove streams + List inLOS = behaviorcontroller.getMemory(MemoryModuleType.VISIBLE_MOBS).orElse(null); + if (inLOS != null) { + double maxRangeSquared = this.getMaxRange(); + for (int index = 0, len = inLOS.size(); index < len; ++index) { + EntityLiving entity = inLOS.get(index); + if (!this.canTarget(entity)) { + continue; + } + double distance = entity.getDistanceSquared(entityliving.locX(), entityliving.locY(), entityliving.locZ()); + if (distance > maxRangeSquared) { + continue; + } + behaviorcontroller.setMemory(MemoryModuleType.INTERACTION_TARGET, entity); // Tuinity - decompile fix + behaviorcontroller.setMemory(MemoryModuleType.LOOK_TARGET, (new BehaviorPositionEntity(entity, true))); // Tuinity - decompile fix + break; + } + } + // Tuinity end - remove streams } + private final boolean canTarget(EntityLiving entityliving) { return this.a(entityliving); } // Tuinity - OBFHELPER private boolean a(EntityLiving entityliving) { return this.b.equals(entityliving.getEntityType()) && this.d.test(entityliving); } diff --git a/src/main/java/net/minecraft/server/BlockBase.java b/src/main/java/net/minecraft/server/BlockBase.java index 1f334d63282bd5c23dc3b275a220f09e60c34537..829d4a7508e1656dbdc912096b7eafcf30cbb5b2 100644 --- a/src/main/java/net/minecraft/server/BlockBase.java +++ b/src/main/java/net/minecraft/server/BlockBase.java @@ -295,21 +295,23 @@ public abstract class BlockBase { public abstract static class BlockData extends IBlockDataHolder { - private final int b; - private final boolean e; + private final int b; public final int getEmittedLight() { return this.b; } // Tuinity - OBFHELPER + private final boolean e; public final boolean isTransparentOnSomeFaces() { return this.e; } // Tuinity - OBFHELPER private final boolean f; private final Material g; private final MaterialMapColor h; public final float strength; private final boolean j; - private final boolean k; + private final boolean k; public final boolean isOpaque() { return this.k; } // Tuinity - OBFHELPER private final BlockBase.e l; private final BlockBase.e m; private final BlockBase.e n; private final BlockBase.e o; private final BlockBase.e p; @Nullable - protected BlockBase.BlockData.Cache a; + protected BlockBase.BlockData.Cache a; protected final BlockBase.BlockData.Cache getShapeCache() { return this.a; } // Tuinity - OBFHELPER + public PathType staticPathType; // Tuinity - cache static path types + public PathType neighbourOverridePathType; // Tuinity - cache static path types protected BlockData(Block block, ImmutableMap, Comparable> immutablemap, MapCodec mapcodec) { super(block, immutablemap, mapcodec); @@ -328,6 +330,7 @@ public abstract class BlockBase { this.n = blockbase_info.s; this.o = blockbase_info.t; this.p = blockbase_info.u; + this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Tuinity } // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData; @@ -343,12 +346,62 @@ public abstract class BlockBase { protected Fluid fluid; // Paper end + // Tuinity start - micro the hell out of this call + protected boolean shapeExceedsCube = true; + public final boolean shapeExceedsCube() { + return this.shapeExceedsCube; + } + // Tuinity end + + // Tuinity start + protected int opacityIfCached = -1; + // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15] + public final int getOpacityIfCached() { + return this.opacityIfCached; + } + + protected final boolean conditionallyFullOpaque; + public final boolean isConditionallyFullOpaque() { + return this.conditionallyFullOpaque; + } + // Tuinity end + public void a() { this.fluid = this.getBlock().d(this.p()); // Paper - moved from getFluid() this.isTicking = this.getBlock().isTicking(this.p()); // Paper - moved from isTicking() if (!this.getBlock().o()) { this.a = new BlockBase.BlockData.Cache(this.p()); } + this.shapeExceedsCube = this.a == null || this.a.c; // Tuinity - moved from actual method to here + this.staticPathType = null; // Tuinity - cache static path type + this.neighbourOverridePathType = null; // Tuinity - cache static path types + this.opacityIfCached = this.a == null || this.isConditionallyFullOpaque() ? -1 : this.a.getOpacity(); // Tuinity - cache opacity for light + // Tuinity start - optimise culling shape cache for light + if (this.a != null && this.a.getCullingShapeCache() != null) { + for (int i = 0, len = this.a.getCullingShapeCache().length; i < len; ++i) { + VoxelShape face = this.a.getCullingShapeCache()[i].simplify(); + if (face.isEmpty()) { + this.a.getCullingShapeCache()[i] = VoxelShapes.getEmptyShape(); + continue; + } + List boxes = face.getBoundingBoxesRepresentation(); + if (boxes.size() == 1) { + AxisAlignedBB boundingBox = boxes.get(0); + if (boundingBox.equals(VoxelShapes.optimisedFullCube.aabb)) { + this.a.getCullingShapeCache()[i] = VoxelShapes.fullCube(); + } else { + this.a.getCullingShapeCache()[i] = VoxelShapes.of(boundingBox); + if (!(this.a.getCullingShapeCache()[i] instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) && + this.a.getCullingShapeCache()[i].getBoundingBoxesRepresentation().size() == 1) { + this.a.getCullingShapeCache()[i] = new com.tuinity.tuinity.voxel.AABBVoxelShape(boundingBox); + } + } + continue; + } + this.a.getCullingShapeCache()[i] = face; + } + } + // Tuinity end - optimise culling shape cache for light } @@ -372,10 +425,12 @@ public abstract class BlockBase { return this.a != null ? this.a.g : this.getBlock().b(this.p(), iblockaccess, blockposition); } + public final int getOpacity(IBlockAccess iblockaccess, BlockPosition blockposition) { return this.b(iblockaccess, blockposition); } // Tuinity - OBFHELPER public int b(IBlockAccess iblockaccess, BlockPosition blockposition) { return this.a != null ? this.a.h : this.getBlock().f(this.p(), iblockaccess, blockposition); } + public final VoxelShape getCullingFace(IBlockAccess iblockaccess, BlockPosition blockposition, EnumDirection enumdirection) { return this.a(iblockaccess, blockposition, enumdirection); } // Tuinity - OBFHELPER public VoxelShape a(IBlockAccess iblockaccess, BlockPosition blockposition, EnumDirection enumdirection) { return this.a != null && this.a.i != null ? this.a.i[enumdirection.ordinal()] : VoxelShapes.a(this.c(iblockaccess, blockposition), enumdirection); } @@ -385,7 +440,7 @@ public abstract class BlockBase { } public final boolean d() { // Paper - return this.a == null || this.a.c; + return this.shapeExceedsCube; // Tuinity - moved into shape cache init } public final boolean e() { // Paper @@ -675,9 +730,9 @@ public abstract class BlockBase { private static final int f = EnumBlockSupport.values().length; protected final boolean a; private final boolean g; - private final int h; + private final int h; private final int getOpacity() { return this.h; } // Tuinity - OBFHELPER @Nullable - private final VoxelShape[] i; + private final VoxelShape[] i; private final VoxelShape[] getCullingShapeCache () { return this.i; } // Tuinity - OBFHELPER protected final VoxelShape b; protected final boolean c; private final boolean[] j; diff --git a/src/main/java/net/minecraft/server/BlockChest.java b/src/main/java/net/minecraft/server/BlockChest.java index 12a0230448dd8d56f6dc20e23cacaf0b8a9433d1..9e5e6de52efabe9126f6c47acb35fa1dc461ff4f 100644 --- a/src/main/java/net/minecraft/server/BlockChest.java +++ b/src/main/java/net/minecraft/server/BlockChest.java @@ -195,7 +195,7 @@ public class BlockChest extends BlockChestAbstract implements I @Override public void remove(IBlockData iblockdata, World world, BlockPosition blockposition, IBlockData iblockdata1, boolean flag) { if (!iblockdata.a(iblockdata1.getBlock())) { - TileEntity tileentity = world.getTileEntity(blockposition); + TileEntity tileentity = world.getTileEntity(blockposition, false); // Tuinity - block has since changed. if (tileentity instanceof IInventory) { InventoryUtils.dropInventory(world, blockposition, (IInventory) tileentity); diff --git a/src/main/java/net/minecraft/server/BlockPosition.java b/src/main/java/net/minecraft/server/BlockPosition.java index 2d887af902a33b0e28d8f0a6ac2e59c815a7856e..2291135eaef64c403183724cb6e413cd7e472672 100644 --- a/src/main/java/net/minecraft/server/BlockPosition.java +++ b/src/main/java/net/minecraft/server/BlockPosition.java @@ -449,10 +449,10 @@ public class BlockPosition extends BaseBlockPosition { } public final BlockPosition.MutableBlockPosition setValues(int i, int j, int k) { return d(i, j, k);} // Paper - OBFHELPER - public BlockPosition.MutableBlockPosition d(int i, int j, int k) { - this.o(i); - this.p(j); - this.q(k); + public final BlockPosition.MutableBlockPosition d(int i, int j, int k) { // Tuinity + ((BaseBlockPosition)this).a = i; // Tuinity - force inline + ((BaseBlockPosition)this).b = j; // Tuinity - force inline + ((BaseBlockPosition)this).e = k; // Tuinity - force inline return this; } @@ -462,12 +462,18 @@ public class BlockPosition extends BaseBlockPosition { } public final BlockPosition.MutableBlockPosition setValues(final BaseBlockPosition baseblockposition) { return this.g(baseblockposition); } // Paper - OBFHELPER - public BlockPosition.MutableBlockPosition g(BaseBlockPosition baseblockposition) { - return this.d(baseblockposition.getX(), baseblockposition.getY(), baseblockposition.getZ()); + public final BlockPosition.MutableBlockPosition g(BaseBlockPosition baseblockposition) { // Tuinity + ((BaseBlockPosition)this).a = baseblockposition.a; // Tuinity - force inline + ((BaseBlockPosition)this).b = baseblockposition.b; // Tuinity - force inline + ((BaseBlockPosition)this).e = baseblockposition.e; // Tuinity - force inline + return this; } - public BlockPosition.MutableBlockPosition g(long i) { - return this.d(b(i), c(i), d(i)); + public final BlockPosition.MutableBlockPosition g(long i) { // Tuinity + ((BaseBlockPosition)this).a = (int)(i >> 38); // Tuinity - force inline + ((BaseBlockPosition)this).b = (int)((i << 52) >> 52); // Tuinity - force inline + ((BaseBlockPosition)this).e = (int)((i << 26) >> 38); // Tuinity - force inline + return this; } public BlockPosition.MutableBlockPosition a(EnumAxisCycle enumaxiscycle, int i, int j, int k) { @@ -482,8 +488,11 @@ public class BlockPosition extends BaseBlockPosition { return this.d(baseblockposition.getX() + i, baseblockposition.getY() + j, baseblockposition.getZ() + k); } - public BlockPosition.MutableBlockPosition c(EnumDirection enumdirection) { - return this.c(enumdirection, 1); + public final BlockPosition.MutableBlockPosition c(EnumDirection enumdirection) { // Tuinity + ((BaseBlockPosition)this).a += enumdirection.getAdjacentX(); // Tuinity - force inline + ((BaseBlockPosition)this).b += enumdirection.getAdjacentY(); // Tuinity - force inline + ((BaseBlockPosition)this).e += enumdirection.getAdjacentZ(); // Tuinity - force inline + return this; } public BlockPosition.MutableBlockPosition c(EnumDirection enumdirection, int i) { @@ -511,21 +520,30 @@ public class BlockPosition extends BaseBlockPosition { } } - /* // Paper start - comment out useless overrides @Override - @Override - public void o(int i) { - super.o(i); + // Tuinity start + // only expose set on the mutable blockpos + public final void setX(int value) { + ((BaseBlockPosition)this).a = value; + } + public final void setY(int value) { + ((BaseBlockPosition)this).b = value; + } + public final void setZ(int value) { + ((BaseBlockPosition)this).e = value; } - @Override - public void p(int i) { - super.p(i); + public final void o(int i) { + ((BaseBlockPosition)this).a = i; // need cast thanks to name conflict + } + + public final void p(int i) { + ((BaseBlockPosition)this).b = i; } - public void q(int i) { - super.q(i); + public final void q(int i) { + ((BaseBlockPosition)this).e = i; } - */ // Paper end + // Tuinity end @Override public BlockPosition immutableCopy() { diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java index af9d54ef057d5f6977cf77c57cde25b6b0d1f39d..7842e65115cf6c37322c83ebff954a06408680b5 100644 --- a/src/main/java/net/minecraft/server/Chunk.java +++ b/src/main/java/net/minecraft/server/Chunk.java @@ -91,6 +91,186 @@ public class Chunk implements IChunkAccess { private final int[] inventoryEntityCounts = new int[16]; // Paper end + // Tuinity start - optimise hard collision handling + final com.destroystokyo.paper.util.maplist.EntityList[] hardCollidingEntities = new com.destroystokyo.paper.util.maplist.EntityList[16]; + + { + for (int i = 0, len = this.hardCollidingEntities.length; i < len; ++i) { + this.hardCollidingEntities[i] = new com.destroystokyo.paper.util.maplist.EntityList(); + } + } + + public final void getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List into, Predicate predicate) { + // copied from getEntities + int min = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D); + int max = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D); + + min = MathHelper.clamp(min, 0, this.hardCollidingEntities.length - 1); + max = MathHelper.clamp(max, 0, this.hardCollidingEntities.length - 1); + + for (int k = min; k <= max; ++k) { + com.destroystokyo.paper.util.maplist.EntityList entityList = this.hardCollidingEntities[k]; + Entity[] entities = entityList.getRawData(); + + for (int i = 0, len = entityList.size(); i < len; ++i) { + Entity entity1 = entities[i]; + if (entity1.shouldBeRemoved) continue; // Paper + + if (entity1 != entity && entity1.getBoundingBox().intersects(axisalignedbb)) { + if (predicate == null || predicate.test(entity1)) { + into.add(entity1); + } + + if (!(entity1 instanceof EntityEnderDragon)) { + continue; + } + + EntityComplexPart[] aentitycomplexpart = ((EntityEnderDragon)entity1).children; + int l = aentitycomplexpart.length; + + for (int i1 = 0; i1 < l; ++i1) { + EntityComplexPart entitycomplexpart = aentitycomplexpart[i1]; + + if (entitycomplexpart != entity && entitycomplexpart.getBoundingBox().intersects(axisalignedbb) && (predicate == null || predicate.test(entitycomplexpart))) { + into.add(entitycomplexpart); + } + } + } + } + } + } + // Tuinity end - optimise hard collision handling + // Tuinity start - rewrite light engine + private volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(); + private volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(); + private volatile boolean[] emptinessMap; + + @Override + public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getEmptinessMap() { + return this.emptinessMap; + } + + @Override + public void setEmptinessMap(boolean[] emptinessMap) { + this.emptinessMap = emptinessMap; + } + // Tuinity end - rewrite light engine + + // Tuinity start - entity slices by class + private final com.tuinity.tuinity.chunk.ChunkEntitiesByClass entitiesByClass = new com.tuinity.tuinity.chunk.ChunkEntitiesByClass(this); + + public boolean hasEntitiesMaybe(Class clazz) { + return this.entitiesByClass.hasEntitiesMaybe(clazz); + } + + public final void getEntitiesClass(Class clazz, Entity entity, AxisAlignedBB boundingBox, Predicate predicate, List into) { + if (!org.bukkit.Bukkit.isPrimaryThread()) { + this.getEntities((Class)clazz, boundingBox, (List)into, (Predicate)predicate); + return; + } + this.entitiesByClass.lookupClass(clazz, entity, boundingBox, predicate, into); + } + // Tuinity end - entity slices by class + + // Tuinity start - optimise checkDespawn + private boolean playerGeneralAreaCacheSet; + private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playerGeneralAreaCache; + + void updateGeneralAreaCache() { + this.updateGeneralAreaCache(((WorldServer)this.world).getChunkProvider().playerChunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey)); + } + + void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) { + this.playerGeneralAreaCacheSet = true; + this.playerGeneralAreaCache = value; + } + + public EntityPlayer findNearestPlayer(Entity to, Predicate predicate) { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; + + if (nearby == null) { + return null; + } + + Object[] backingSet = nearby.getBackingSet(); + double closestDistance = Double.MAX_VALUE; + EntityPlayer closest = null; + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof EntityPlayer)) { + continue; + } + EntityPlayer player = (EntityPlayer)_player; + + double distance = to.getDistanceSquared(player.locX(), player.locY(), player.locZ()); + if (distance < closestDistance && predicate.test(player)) { + closest = player; + closestDistance = distance; + } + } + + return closest; + } + + public void getNearestPlayers(Entity source, Predicate predicate, double range, List ret) { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; + + if (nearby == null) { + return; + } + + double rangeSquared = range * range; + + Object[] backingSet = nearby.getBackingSet(); + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof EntityPlayer)) { + continue; + } + EntityPlayer player = (EntityPlayer)_player; + + if (range >= 0.0) { + double distanceSquared = player.getDistanceSquared(source.locX(), source.locY(), source.locZ()); + if (distanceSquared > rangeSquared) { + continue; + } + } + + if (predicate == null || predicate.test(player)) { + ret.add(player); + } + } + } + // Tuinity end - optimise checkDespawn + public Chunk(World world, ChunkCoordIntPair chunkcoordintpair, BiomeStorage biomestorage, ChunkConverter chunkconverter, TickList ticklist, TickList ticklist1, long i, @Nullable ChunkSection[] achunksection, @Nullable Consumer consumer) { this.sections = new ChunkSection[16]; this.e = Maps.newHashMap(); @@ -298,6 +478,11 @@ public class Chunk implements IChunkAccess { public Chunk(World world, ProtoChunk protochunk) { this(world, protochunk.getPos(), protochunk.getBiomeIndex(), protochunk.p(), protochunk.n(), protochunk.o(), protochunk.getInhabitedTime(), protochunk.getSections(), (Consumer) null); + // Tuinity start - copy over protochunk light + this.blockNibbles = protochunk.getBlockNibbles(); + this.skyNibbles = protochunk.getSkyNibbles(); + this.emptinessMap = protochunk.getEmptinessMap(); + // Tuinity end - copy over protochunk light Iterator iterator = protochunk.y().iterator(); while (iterator.hasNext()) { @@ -548,6 +733,7 @@ public class Chunk implements IChunkAccess { @Override public void a(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("Chunk add entity"); // Tuinity this.q = true; int i = MathHelper.floor(entity.locX() / 16.0D); int j = MathHelper.floor(entity.locZ() / 16.0D); @@ -593,8 +779,8 @@ public class Chunk implements IChunkAccess { entity.chunkX = this.loc.x; entity.chunkY = k; entity.chunkZ = this.loc.z; - this.entities.add(entity); // Paper - per chunk entity list - this.entitySlices[k].add(entity); + this.entities.add(entity); this.entitiesByClass.addEntity(entity, entity.chunkY); // Paper - per chunk entity list // Tuinity - entities by class + this.entitySlices[k].add(entity); if (entity.hardCollides()) this.hardCollidingEntities[k].add(entity); // Tuinity - optimise hard colliding entities // Paper start if (entity instanceof EntityItem) { itemCounts[k]++; @@ -617,6 +803,7 @@ public class Chunk implements IChunkAccess { } public void a(Entity entity, int i) { + org.spigotmc.AsyncCatcher.catchOp("Chunk remove entity"); // Tuinity // Tuinity if (i < 0) { i = 0; } @@ -631,7 +818,7 @@ public class Chunk implements IChunkAccess { entity.entitySlice = null; entity.inChunk = false; } - if (!this.entitySlices[i].remove(entity)) { + if (entity.hardCollides()) this.hardCollidingEntities[i].remove(entity); this.entitiesByClass.removeEntity(entity, i); if (!this.entitySlices[i].remove(entity)) { // Tuinity - optimise hard colliding entities // Tuinity - entities by class return; } if (entity instanceof EntityItem) { @@ -944,6 +1131,7 @@ public class Chunk implements IChunkAccess { } + public final void getEntities(Class oclass, AxisAlignedBB axisalignedbb, List list, @Nullable Predicate predicate) { this.a(oclass, axisalignedbb, list, predicate); } // Tuinity - OBFHELPER public void a(Class oclass, AxisAlignedBB axisalignedbb, List list, @Nullable Predicate predicate) { org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D); diff --git a/src/main/java/net/minecraft/server/ChunkCache.java b/src/main/java/net/minecraft/server/ChunkCache.java index 8eecdcde510661ec3a13a25a04ba394f6b6dc012..ab1085091fefea3a3fa15f7028bec050d00a6f5e 100644 --- a/src/main/java/net/minecraft/server/ChunkCache.java +++ b/src/main/java/net/minecraft/server/ChunkCache.java @@ -1,5 +1,6 @@ package net.minecraft.server; +import java.util.List; import java.util.function.Predicate; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -12,6 +13,156 @@ public class ChunkCache implements IBlockAccess, ICollisionAccess { protected boolean d; protected final World e; protected final World getWorld() { return e; } // Paper - OBFHELPER + // Tuinity start - optimise pathfinder collision detection + @Override + public boolean getCubes(Entity entity) { + return !this.getCollisionsForBlocksOrWorldBorder(entity, entity.getBoundingBox(), null, true, null); + } + + @Override + public boolean getCubes(Entity entity, AxisAlignedBB axisalignedbb) { + return !this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, true, null); + } + + @Override + public boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + return !this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, true, null); + } + + public boolean getCollisionsForBlocksOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List list, + boolean collidesWithUnloaded, + java.util.function.BiPredicate predicate) { + boolean ret = false; + final boolean checkOnly = true; + + if (entity != null) { + if (this.getWorldBorder().isAlmostCollidingOnBorder(axisalignedbb)) { + if (checkOnly) { + return true; + } else { + VoxelShapes.addBoxesTo(this.getWorldBorder().getCollisionShape(), list); + ret = true; + } + } + } + + int minBlockX = MathHelper.floor(axisalignedbb.minX - MCUtil.COLLISION_EPSILON) - 1; + int maxBlockX = MathHelper.floor(axisalignedbb.maxX + MCUtil.COLLISION_EPSILON) + 1; + + int minBlockY = MathHelper.floor(axisalignedbb.minY - MCUtil.COLLISION_EPSILON) - 1; + int maxBlockY = MathHelper.floor(axisalignedbb.maxY + MCUtil.COLLISION_EPSILON) + 1; + + int minBlockZ = MathHelper.floor(axisalignedbb.minZ - MCUtil.COLLISION_EPSILON) - 1; + int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + MCUtil.COLLISION_EPSILON) + 1; + + + BlockPosition.MutableBlockPosition mutablePos = new BlockPosition.MutableBlockPosition(); + VoxelShapeCollision collisionShape = null; + + // special cases: + if (minBlockY > 255 || maxBlockY < 0) { + // no point in checking + return ret; + } + + int minYIterate = Math.max(0, minBlockY); + int maxYIterate = Math.min(255, maxBlockY); + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + // TODO special case single chunk? + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk + int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk + + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk + int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk + + int chunkXGlobalPos = currChunkX << 4; + int chunkZGlobalPos = currChunkZ << 4; + Chunk chunk = (Chunk)this.getChunkIfLoaded(currChunkX, currChunkZ); + + if (chunk == null) { + if (collidesWithUnloaded) { + if (checkOnly) { + return true; + } else { + list.add(AxisAlignedBB.getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + } + } + continue; + } + + ChunkSection[] sections = chunk.getSections(); + + // bound y + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + ChunkSection section = sections[currY >>> 4]; + if (section == null || section.isFullOfAir()) { + // empty + // skip to next section + currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one + continue; + } + + DataPaletteBlock blocks = section.blockIds; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8); + int blockX = currX | chunkXGlobalPos; + int blockY = currY; + int blockZ = currZ | chunkZGlobalPos; + + int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + + ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + + ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); + if (edgeCount == 3) { + continue; + } + + IBlockData blockData = blocks.rawGet(localBlockIndex); + + if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { + mutablePos.setValues(blockX, blockY, blockZ); + if (collisionShape == null) { + collisionShape = entity == null ? VoxelShapeCollision.a() : VoxelShapeCollision.a(entity); + } + VoxelShape voxelshape2 = blockData.getCollisionShape(this, mutablePos, collisionShape); + if (voxelshape2 != VoxelShapes.getEmptyShape()) { + VoxelShape voxelshape3 = voxelshape2.offset((double)blockX, (double)blockY, (double)blockZ); + + if (predicate != null && !predicate.test(blockData, mutablePos)) { + continue; + } + + if (checkOnly) { + if (voxelshape3.intersects(axisalignedbb)) { + return true; + } + } else { + ret |= VoxelShapes.addBoxesToIfIntersects(voxelshape3, axisalignedbb, list); + } + } + } + } + } + } + } + } + + return ret; + } + // Tuinity end - optimise pathfinder collision detection + public ChunkCache(World world, BlockPosition blockposition, BlockPosition blockposition1) { this.e = world; this.a = blockposition.getX() >> 4; diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java index 3c7b225edbe23dc1959002293a6f8b816287b5a8..f1c686810fb4e9c05df45d664c93af73d17f0624 100644 --- a/src/main/java/net/minecraft/server/ChunkMapDistance.java +++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java @@ -31,7 +31,7 @@ public abstract class ChunkMapDistance { private static final int b = 33 + ChunkStatus.a(ChunkStatus.FULL) - 2; private final Long2ObjectMap> c = new Long2ObjectOpenHashMap(); public final Long2ObjectOpenHashMap>> tickets = new Long2ObjectOpenHashMap(); - private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a(); + private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a(); final ChunkMapDistance.a getTicketTracker() { return this.ticketLevelTracker; } // Tuinity - OBFHELPER public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used private final ChunkMapDistance.c g = new ChunkMapDistance.c(33); // Paper start use a queue, but still keep unique requirement @@ -53,6 +53,47 @@ public abstract class ChunkMapDistance { PlayerChunkMap chunkMap; // Paper + // Tuinity start - delay chunk unloads + private long nextUnloadId; // delay chunk unloads + private final Long2ObjectOpenHashMap> delayedChunks = new Long2ObjectOpenHashMap<>(); + public final void removeTickets(long chunk, TicketType type) { + ArraySetSorted> tickets = this.tickets.get(chunk); + if (tickets == null) { + return; + } + if (type == TicketType.DELAYED_UNLOAD) { + this.delayedChunks.remove(chunk); + } + boolean changed = tickets.removeIf((Ticket ticket) -> { + return ticket.getTicketType() == type; + }); + if (changed) { + this.getTicketTracker().update(chunk, getLowestTicketLevel(tickets), false); + } + } + + private final java.util.function.LongFunction> computeFuntion = (long key) -> { + Ticket ret = new Ticket<>(TicketType.DELAYED_UNLOAD, -1, ++ChunkMapDistance.this.nextUnloadId); + ret.isCached = true; + return ret; + }; + + private void computeDelayedTicketFor(long chunk, int removedLevel, ArraySetSorted> tickets) { + int lowestLevel = getLowestTicketLevel(tickets); + if (removedLevel > lowestLevel) { + return; + } + final Ticket ticket = this.delayedChunks.computeIfAbsent(chunk, this.computeFuntion); + if (ticket.getTicketLevel() != -1) { + // since we modify data used in sorting, we need to remove before + tickets.remove(ticket); + } + ticket.setCreationTick(this.currentTick); + ticket.setTicketLevel(removedLevel); + tickets.add(ticket); // re-add with new expire time and ticket level + } + // Tuinity end - delay chunk unloads + protected ChunkMapDistance(Executor executor, Executor executor1) { executor1.getClass(); Mailbox mailbox = Mailbox.a("player ticket throttler", executor1::execute); @@ -65,21 +106,45 @@ public abstract class ChunkMapDistance { } protected void purgeTickets() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async purge tickets"); // Tuinity ++this.currentTick; ObjectIterator objectiterator = this.tickets.long2ObjectEntrySet().fastIterator(); + // Tuinity start - delay chunk unloads + int[] tempLevel = new int[] { PlayerChunkMap.GOLDEN_TICKET + 1 }; + Entry>>[] entryPass = new Entry[1]; + java.util.function.Predicate> isExpired = (ticket) -> { // CraftBukkit - decompile error + // Tuinity start - delay chunk unloads + boolean ret = ticket.isExpired(this.currentTick); + if (com.tuinity.tuinity.config.TuinityConfig.delayChunkUnloadsBy <= 0) { + return ret; + } + if (ret && ticket.getTicketType().delayUnloadViable && ticket.getTicketLevel() < tempLevel[0]) { + tempLevel[0] = ticket.getTicketLevel(); + } + if (ret && ticket.getTicketType() == TicketType.DELAYED_UNLOAD && ticket.isCached) { + this.delayedChunks.remove(entryPass[0].getLongKey(), ticket); // clean up ticket... + } + return ret; + }; + // Tuinity end - delay chunk unloads while (objectiterator.hasNext()) { - Entry>> entry = (Entry) objectiterator.next(); + Entry>> entry = (Entry) objectiterator.next(); entryPass[0] = entry; // Tuinity - only allocate lambda once - if ((entry.getValue()).removeIf((ticket) -> { // CraftBukkit - decompile error - return ticket.b(this.currentTick); - })) { + if ((entry.getValue()).removeIf(isExpired)) { // Tuinity - move above - only allocate once + // Tuinity start - delay chunk unloads + if (tempLevel[0] < (PlayerChunkMap.GOLDEN_TICKET + 1)) { + this.computeDelayedTicketFor(entry.getLongKey(), tempLevel[0], entry.getValue()); + } + // Tuinity end - delay chunk unloads this.ticketLevelTracker.update(entry.getLongKey(), getLowestTicketLevel((ArraySetSorted) entry.getValue()), false); } if (((ArraySetSorted) entry.getValue()).isEmpty()) { objectiterator.remove(); } + + tempLevel[0] = PlayerChunkMap.GOLDEN_TICKET + 1; // Tuinity - reset } } @@ -98,6 +163,7 @@ public abstract class ChunkMapDistance { protected abstract PlayerChunk a(long i, int j, @Nullable PlayerChunk playerchunk, int k); public boolean a(PlayerChunkMap playerchunkmap) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot tick ChunkMapDistance off of the main-thread");// Tuinity //this.f.a(); // Paper - no longer used AsyncCatcher.catchOp("DistanceManagerTick"); // Paper this.g.a(); @@ -176,27 +242,11 @@ public abstract class ChunkMapDistance { boolean removed = false; // CraftBukkit if (arraysetsorted.remove(ticket)) { removed = true; // CraftBukkit - // Paper start - delay chunk unloads for player tickets - long delayChunkUnloadsBy = chunkMap.world.paperConfig.delayChunkUnloadsBy; - if (ticket.getTicketType() == TicketType.PLAYER && delayChunkUnloadsBy > 0) { - boolean hasPlayer = false; - for (Ticket ticket1 : arraysetsorted) { - if (ticket1.getTicketType() == TicketType.PLAYER) { - hasPlayer = true; - break; - } - } - PlayerChunk playerChunk = chunkMap.getUpdatingChunk(i); - if (!hasPlayer && playerChunk != null && playerChunk.isFullChunkReady()) { - Ticket delayUnload = new Ticket(TicketType.DELAY_UNLOAD, 33, i); - delayUnload.delayUnloadBy = delayChunkUnloadsBy; - delayUnload.setCurrentTick(this.currentTick); - arraysetsorted.remove(delayUnload); - // refresh ticket - arraysetsorted.add(delayUnload); - } + // Tuinity start - delay chunk unloads + if (com.tuinity.tuinity.config.TuinityConfig.delayChunkUnloadsBy > 0 && ticket.getTicketType().delayUnloadViable) { + this.computeDelayedTicketFor(i, ticket.getTicketLevel(), arraysetsorted); } - // Paper end + // Tuinity end - delay chunk unloads } if (arraysetsorted.isEmpty()) { @@ -370,6 +420,7 @@ public abstract class ChunkMapDistance { } private ArraySetSorted> e(long i) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async tickets compute"); // Tuinity return (ArraySetSorted) this.tickets.computeIfAbsent(i, (j) -> { return ArraySetSorted.a(4); }); @@ -387,6 +438,7 @@ public abstract class ChunkMapDistance { } public void a(SectionPosition sectionposition, EntityPlayer entityplayer) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async player add"); // Tuinity long i = sectionposition.r().pair(); ((ObjectSet) this.c.computeIfAbsent(i, (j) -> { @@ -397,6 +449,7 @@ public abstract class ChunkMapDistance { } public void b(SectionPosition sectionposition, EntityPlayer entityplayer) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async player remove"); // Tuinity long i = sectionposition.r().pair(); ObjectSet objectset = (ObjectSet) this.c.get(i); @@ -446,6 +499,7 @@ public abstract class ChunkMapDistance { // CraftBukkit start public void removeAllTicketsFor(TicketType ticketType, int ticketLevel, T ticketIdentifier) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async ticket remove"); // Tuinity Ticket target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier); for (java.util.Iterator>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) { diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index 75d25576d68ec95a14372f8530f4916f2bd7c3c5..38ca1c042afd41a1f660f88e398fedde00f34e39 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -22,6 +22,12 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Paper import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +// Tuinity start +import it.unimi.dsi.fastutil.objects.Object2BooleanLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +// Tuinity end + public class ChunkProviderServer extends IChunkProvider { private static final List b = ChunkStatus.a(); static final List getPossibleChunkStatuses() { return ChunkProviderServer.b; } // Paper - OBFHELPER @@ -112,7 +118,7 @@ public class ChunkProviderServer extends IChunkProvider { return (Chunk)this.getChunkAt(x, z, ChunkStatus.FULL, true); } - private long chunkFutureAwaitCounter; + long chunkFutureAwaitCounter; // Tuinity - private -> package private public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer onLoad) { if (Thread.currentThread() != this.serverThread) { @@ -174,9 +180,9 @@ public class ChunkProviderServer extends IChunkProvider { try { if (onLoad != null) { - playerChunkMap.callbackExecutor.execute(() -> { + // Tuinity - revert incorrect use of callback executor onLoad.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback. - }); + // Tuinity - revert incorrect use of callback executor } } catch (Throwable thr) { if (thr instanceof ThreadDeath) { @@ -201,6 +207,165 @@ public class ChunkProviderServer extends IChunkProvider { } // Paper end - rewrite ticklistserver + // Tuinity start + // this will try to avoid chunk neighbours for lighting + public final IChunkAccess getFullStatusChunkAt(int chunkX, int chunkZ) { + Chunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + + IChunkAccess empty = this.getChunkAt(chunkX, chunkZ, ChunkStatus.EMPTY, true); + if (empty != null && empty.getChunkStatus() == ChunkStatus.FULL) { + return empty; + } + return this.getChunkAt(chunkX, chunkZ, ChunkStatus.FULL, true); + } + + public final IChunkAccess getFullStatusChunkAtIfLoaded(int chunkX, int chunkZ) { + Chunk ifLoaded = this.getChunkAtIfLoadedImmediately(chunkX, chunkZ); + if (ifLoaded != null) { + return ifLoaded; + } + + IChunkAccess ret = this.getChunkAtImmediately(chunkX, chunkZ); + if (ret != null && ret.getChunkStatus() == ChunkStatus.FULL) { + return ret; + } else { + return null; + } + } + + void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, + java.util.function.Consumer consumer) { + this.getChunkAtAsynchronously(chunkX, chunkZ, ticketLevel, (PlayerChunk playerChunk) -> { + if (ticketLevel <= 33) { + return (CompletableFuture)playerChunk.getFullChunkFuture(); + } else { + return playerChunk.getOrCreateFuture(PlayerChunk.getChunkStatus(ticketLevel), ChunkProviderServer.this.playerChunkMap); + } + }, consumer); + } + + void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel, + java.util.function.Function>> function, + java.util.function.Consumer consumer) { + if (Thread.currentThread() != this.serverThread) { + throw new IllegalStateException(); + } + ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(chunkX, chunkZ); + Long identifier = Long.valueOf(this.chunkFutureAwaitCounter++); + this.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier); + this.tickDistanceManager(); + + PlayerChunk chunk = this.playerChunkMap.getUpdatingChunk(chunkPos.pair()); + + if (chunk == null) { + throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.world.getWorld().getName() + "'"); + } + + CompletableFuture> future = function.apply(chunk); + + future.whenCompleteAsync((either, throwable) -> { + try { + if (throwable != null) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + MinecraftServer.LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "'", throwable); + } else if (either.right().isPresent()) { + MinecraftServer.LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "': " + either.right().get().toString()); + } + + try { + if (consumer != null) { + consumer.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback. + } + } catch (Throwable thr) { + if (thr instanceof ThreadDeath) { + throw (ThreadDeath)thr; + } + MinecraftServer.LOGGER.fatal("Load callback for future await failed " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "'", thr); + return; + } + } finally { + // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback. + ChunkProviderServer.this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); + ChunkProviderServer.this.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier); + } + }, this.serverThreadQueue); + } + + void chunkLoadAccept(int chunkX, int chunkZ, IChunkAccess chunk, java.util.function.Consumer consumer) { + try { + consumer.accept(chunk); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + MinecraftServer.LOGGER.error("Load callback for chunk " + chunkX + "," + chunkZ + " in world '" + this.world.getWorld().getName() + "' threw an exception", throwable); + } + } + + public final void getChunkAtAsynchronously(int chunkX, int chunkZ, ChunkStatus status, boolean gen, boolean allowSubTicketLevel, java.util.function.Consumer onLoad) { + // try to fire sync + int chunkStatusTicketLevel = 33 + ChunkStatus.getTicketLevelOffset(status); + PlayerChunk playerChunk = this.playerChunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(chunkX, chunkZ)); + if (playerChunk != null) { + ChunkStatus holderStatus = playerChunk.getChunkHolderStatus(); + IChunkAccess immediate = playerChunk.getAvailableChunkNow(); + if (immediate != null) { + if (allowSubTicketLevel ? immediate.getChunkStatus().isAtLeastStatus(status) : (playerChunk.getTicketLevel() <= chunkStatusTicketLevel && holderStatus != null && holderStatus.isAtLeastStatus(status))) { + this.chunkLoadAccept(chunkX, chunkZ, immediate, onLoad); + return; + } else { + if (gen || (!allowSubTicketLevel && immediate.getChunkStatus().isAtLeastStatus(status))) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } else { + this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); + return; + } + } + } + } + + // need to fire async + + if (gen && !allowSubTicketLevel) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } + + this.getChunkAtAsynchronously(chunkX, chunkZ, MCUtil.getTicketLevelFor(ChunkStatus.EMPTY), (IChunkAccess chunk) -> { + if (chunk == null) { + throw new IllegalStateException("Chunk cannot be null"); + } + + if (!chunk.getChunkStatus().isAtLeastStatus(status)) { + if (gen) { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } else { + ChunkProviderServer.this.chunkLoadAccept(chunkX, chunkZ, null, onLoad); + return; + } + } else { + if (allowSubTicketLevel) { + ChunkProviderServer.this.chunkLoadAccept(chunkX, chunkZ, chunk, onLoad); + return; + } else { + this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad); + return; + } + } + }); + } + + final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet tickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entityTickingChunks = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + // Tuinity end + public ChunkProviderServer(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, ChunkGenerator chunkgenerator, int i, boolean flag, WorldLoadListener worldloadlistener, Supplier supplier) { this.world = worldserver; this.serverThreadQueue = new ChunkProviderServer.a(worldserver); @@ -536,6 +701,8 @@ public class ChunkProviderServer extends IChunkProvider { Arrays.fill(this.cacheChunk, (Object) null); } + private long syncLoadCounter; // Tuinity - prevent plugin unloads from removing our ticket + private CompletableFuture> getChunkFutureMainThread(int i, int j, ChunkStatus chunkstatus, boolean flag) { // Paper start - add isUrgent - old sig left in place for dirty nms plugins return getChunkFutureMainThread(i, j, chunkstatus, flag, false); @@ -554,9 +721,12 @@ public class ChunkProviderServer extends IChunkProvider { PlayerChunk.State currentChunkState = PlayerChunk.getChunkState(playerchunk.getTicketLevel()); currentlyUnloading = (oldChunkState.isAtLeast(PlayerChunk.State.BORDER) && !currentChunkState.isAtLeast(PlayerChunk.State.BORDER)); } + final Long identifier; // Tuinity - prevent plugin unloads from removing our ticket if (flag && !currentlyUnloading) { // CraftBukkit end this.chunkMapDistance.a(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); + identifier = Long.valueOf(this.syncLoadCounter++); // Tuinity - prevent plugin unloads from removing our ticket + this.chunkMapDistance.addTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity - prevent plugin unloads from removing our ticket if (isUrgent) this.chunkMapDistance.markUrgent(chunkcoordintpair); // Paper if (this.a(playerchunk, l)) { GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler(); @@ -567,12 +737,20 @@ public class ChunkProviderServer extends IChunkProvider { playerchunk = this.getChunk(k); gameprofilerfiller.exit(); if (this.a(playerchunk, l)) { + this.chunkMapDistance.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); // Tuinity throw (IllegalStateException) SystemUtils.c((Throwable) (new IllegalStateException("No chunk holder after ticket has been added"))); } } - } + } else { identifier = null; } // Tuinity - prevent plugin unloads from removing our ticket // Paper start CompletableFuture> future = this.a(playerchunk, l) ? PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE : playerchunk.a(chunkstatus, this.playerChunkMap); + // Tuinity start - prevent plugin unloads from removing our ticket + if (flag && !currentlyUnloading) { + future.thenAcceptAsync((either) -> { + ChunkProviderServer.this.chunkMapDistance.removeTicketAtLevel(TicketType.REQUIRED_LOAD, chunkcoordintpair, l, identifier); + }, ChunkProviderServer.this.serverThreadQueue); + } + // Tuinity end - prevent plugin unloads from removing our ticket if (isUrgent) { future.thenAccept(either -> this.chunkMapDistance.clearUrgent(chunkcoordintpair)); } @@ -591,8 +769,8 @@ public class ChunkProviderServer extends IChunkProvider { return !this.a(playerchunk, k); } - @Override - public IBlockAccess c(int i, int j) { + public final IBlockAccess getFeaturesReadyChunk(int x, int z) { return this.c(x, z); } // Tuinity - OBFHELPER + @Override public IBlockAccess c(int i, int j) { // Tuinity - OBFHELPER long k = ChunkCoordIntPair.pair(i, j); PlayerChunk playerchunk = this.getChunk(k); @@ -629,6 +807,8 @@ public class ChunkProviderServer extends IChunkProvider { public boolean tickDistanceManager() { // Paper - private -> public if (chunkMapDistance.delayDistanceManagerTick) return false; // Paper + if (this.playerChunkMap.unloadingPlayerChunk) { MinecraftServer.LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity + co.aikar.timings.MinecraftTimings.distanceManagerTick.startTiming(); try { // Tuinity - add timings for distance manager boolean flag = this.chunkMapDistance.a(this.playerChunkMap); boolean flag1 = this.playerChunkMap.b(); @@ -638,6 +818,7 @@ public class ChunkProviderServer extends IChunkProvider { this.clearCache(); return true; } + } finally { co.aikar.timings.MinecraftTimings.distanceManagerTick.stopTiming(); } // Tuinity - add timings for distance manager } public final boolean isInEntityTickingChunk(Entity entity) { return this.a(entity); } // Paper - OBFHELPER @@ -726,7 +907,7 @@ public class ChunkProviderServer extends IChunkProvider { this.world.getMethodProfiler().enter("purge"); this.world.timings.doChunkMap.startTiming(); // Spigot this.chunkMapDistance.purgeTickets(); - this.world.getMinecraftServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.tickDistanceManager(); this.world.timings.doChunkMap.stopTiming(); // Spigot this.world.getMethodProfiler().exitEnter("chunks"); @@ -736,7 +917,7 @@ public class ChunkProviderServer extends IChunkProvider { this.world.timings.doChunkUnload.startTiming(); // Spigot this.world.getMethodProfiler().exitEnter("unload"); this.playerChunkMap.unloadChunks(booleansupplier); - this.world.getMinecraftServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.world.timings.doChunkUnload.stopTiming(); // Spigot this.world.getMethodProfiler().exit(); this.clearCache(); @@ -813,19 +994,23 @@ public class ChunkProviderServer extends IChunkProvider { //List list = Lists.newArrayList(this.playerChunkMap.f()); // Paper //Collections.shuffle(list); // Paper // Paper - moved up - final int[] chunksTicked = {0}; this.playerChunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping - Optional optional = ((Either) playerchunk.a().getNow(PlayerChunk.UNLOADED_CHUNK)).left(); - - if (optional.isPresent()) { + // Tuinity start - optimise chunk tick iteration + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickingChunks.iterator(); + try { + while (iterator.hasNext()) { + Chunk chunk = iterator.next(); + PlayerChunk playerchunk = chunk.playerChunk; + if (playerchunk != null) { // make sure load event has been called along with the load logic we put there + // Tuinity end - optimise chunk tick iteration this.world.getMethodProfiler().enter("broadcast"); this.world.timings.broadcastChunkUpdates.startTiming(); // Paper - timings - playerchunk.a((Chunk) optional.get()); + playerchunk.a(chunk); // Tuinity this.world.timings.broadcastChunkUpdates.stopTiming(); // Paper - timings this.world.getMethodProfiler().exit(); - Optional optional1 = ((Either) playerchunk.b().getNow(PlayerChunk.UNLOADED_CHUNK)).left(); + // Tuinity - if (optional1.isPresent()) { - Chunk chunk = (Chunk) optional1.get(); + if (true) { // Tuinity + // Tuinity ChunkCoordIntPair chunkcoordintpair = playerchunk.i(); if (!this.playerChunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange @@ -837,11 +1022,15 @@ public class ChunkProviderServer extends IChunkProvider { this.world.timings.chunkTicks.startTiming(); // Spigot // Paper this.world.a(chunk, k); this.world.timings.chunkTicks.stopTiming(); // Spigot // Paper - if (chunksTicked[0]++ % 10 == 0) this.world.getMinecraftServer().midTickLoadChunks(); // Paper + MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - exec chunk tasks during world tick } } } - }); + } // Tuinity start - optimise chunk tick iteration + } finally { + iterator.finishedIterating(); + } + // Tuinity end - optimise chunk tick iteration this.world.getMethodProfiler().enter("customSpawners"); if (flag1) { try (co.aikar.timings.Timing ignored = this.world.timings.miscMobSpawning.startTiming()) { // Paper - timings @@ -853,7 +1042,25 @@ public class ChunkProviderServer extends IChunkProvider { this.world.getMethodProfiler().exit(); } + // Tuinity start - controlled flush for entity tracker packets + List disabledFlushes = new java.util.ArrayList<>(this.world.getPlayers().size()); + for (EntityPlayer player : this.world.getPlayers()) { + PlayerConnection connection = player.playerConnection; + if (connection != null) { + connection.networkManager.disableAutomaticFlush(); + disabledFlushes.add(connection.networkManager); + } + } + try { + // Tuinity end - controlled flush for entity tracker packets this.playerChunkMap.g(); + // Tuinity start - controlled flush for entity tracker packets + } finally { + for (NetworkManager networkManager : disabledFlushes) { + networkManager.enableAutomaticFlush(); + } + } + // Tuinity end - controlled flush for entity tracker packets } private void a(long i, Consumer consumer) { @@ -993,51 +1200,18 @@ public class ChunkProviderServer extends IChunkProvider { ChunkProviderServer.this.world.getMethodProfiler().c("runTask"); super.executeTask(runnable); } - - // Paper start - private long lastMidTickChunkTask = 0; - public boolean pollChunkLoadTasks() { - if (com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ChunkProviderServer.this.world.asyncChunkTaskManager.pollNextChunkTask()) { - try { - ChunkProviderServer.this.tickDistanceManager(); - } finally { - // from below: process pending Chunk loadCallback() and unloadCallback() after each run task - playerChunkMap.callbackExecutor.run(); - } - return true; - } - return false; - } - public void midTickLoadChunks() { - MinecraftServer server = ChunkProviderServer.this.world.getMinecraftServer(); - // always try to load chunks, restrain generation/other updates only. don't count these towards tick count - //noinspection StatementWithEmptyBody - while (pollChunkLoadTasks()) {} - - if (System.nanoTime() - lastMidTickChunkTask < 200000) { - return; - } - - for (;server.midTickChunksTasksRan < com.destroystokyo.paper.PaperConfig.midTickChunkTasks && server.canSleepForTick();) { - if (this.executeNext()) { - server.midTickChunksTasksRan++; - lastMidTickChunkTask = System.nanoTime(); - } else { - break; - } - } - } - // Paper end + // Tuinity - replace logic @Override protected boolean executeNext() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot execute chunk tasks off-main thread");// Tuinity // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task try { boolean execChunkTask = com.destroystokyo.paper.io.chunk.ChunkTaskManager.pollChunkWaitQueue() || ChunkProviderServer.this.world.asyncChunkTaskManager.pollNextChunkTask(); // Paper if (ChunkProviderServer.this.tickDistanceManager()) { return true; } else { - //ChunkProviderServer.this.lightEngine.queueUpdate(); // Paper - not needed + ChunkProviderServer.this.lightEngine.queueUpdate(); // Paper - not needed // Tuinity - prevent queue overflow when no players are in this world return super.executeNext() || execChunkTask; // Paper } } finally { diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java index f51bf71c8d6eef3c054ac64765709794fcfad5ee..076d6c1e1cc049dd312ecb30518e7b25fc2d7371 100644 --- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java +++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java @@ -24,6 +24,14 @@ public class ChunkRegionLoader { private static final Logger LOGGER = LogManager.getLogger(); + // Tuinity start + // TODO: Check on update + public static long getLastWorldSaveTime(NBTTagCompound chunkData) { + NBTTagCompound levelData = chunkData.getCompound("Level"); + return levelData.getLong("LastUpdate"); + } + // Tuinity end + // Paper start - guard against serializing mismatching coordinates // TODO Note: This needs to be re-checked each update public static ChunkCoordIntPair getChunkCoordinate(NBTTagCompound chunkData) { @@ -56,6 +64,13 @@ public class ChunkRegionLoader { private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion"); // Paper end + // Tuinity start - rewrite light engine + private static final int STARLIGHT_LIGHT_VERSION = 4; + + private static final String UNINITIALISED_SKYLIGHT_TAG = "starlight.skylight_uninit"; + private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; + // Tuinity end - rewrite light engine + public static InProgressChunkHolder loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound, boolean distinguish) { ArrayDeque tasksToExecuteOnMain = new ArrayDeque<>(); // Paper end @@ -85,13 +100,17 @@ public class ChunkRegionLoader { ProtoChunkTickList protochunkticklist1 = new ProtoChunkTickList<>((fluidtype) -> { return fluidtype == null || fluidtype == FluidTypes.EMPTY; }, chunkcoordintpair, nbttagcompound1.getList("LiquidsToBeTicked", 9)); - boolean flag = nbttagcompound1.getBoolean("isLightOn"); + boolean flag = (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION : nbttagcompound1.getBoolean("isLightOn")); boolean canUseSkyLight = flag && getStatus(nbttagcompound).isAtLeastStatus(ChunkStatus.LIGHT); boolean canUseBlockLight = canUseSkyLight; // Tuinity NBTTagList nbttaglist = nbttagcompound1.getList("Sections", 10); boolean flag1 = true; ChunkSection[] achunksection = new ChunkSection[16]; boolean flag2 = worldserver.getDimensionManager().hasSkyLight(); ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider(); LightEngine lightengine = chunkproviderserver.getLightEngine(); + com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(worldserver); // Tuinity - replace light impl + com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(worldserver); // Tuinity - replace light impl + final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(worldserver); + final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(worldserver); if (flag) { tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main @@ -119,6 +138,7 @@ public class ChunkRegionLoader { if (flag) { if (nbttagcompound2.hasKeyOfType("BlockLight", 7)) { + if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && canUseBlockLight) blockNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(nbttagcompound2.getByteArray("BlockLight").clone()); // Tuinity - replace light impl // Paper start - delay this task since we're executing off-main NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight")); tasksToExecuteOnMain.add(() -> { @@ -128,13 +148,14 @@ public class ChunkRegionLoader { } if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) { + if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && canUseSkyLight) skyNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(nbttagcompound2.getByteArray("SkyLight").clone()); // Tuinity - replace light impl // Paper start - delay this task since we're executing off-main NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight")); tasksToExecuteOnMain.add(() -> { lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight, true); }); // Paper end - delay this task since we're executing off-main - } + } else if (flag2 && nbttagcompound2.getBoolean(UNINITIALISED_SKYLIGHT_TAG)) skyNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(); // Tuinity - replace light impl } } @@ -173,8 +194,12 @@ public class ChunkRegionLoader { object = new Chunk(worldserver.getMinecraftWorld(), chunkcoordintpair, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, j, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys. createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here );// Paper end + ((Chunk)object).setBlockNibbles(blockNibbles); // Tuinity - replace light impl + ((Chunk)object).setSkyNibbles(skyNibbles); // Tuinity - replace light impl } else { ProtoChunk protochunk = new ProtoChunk(chunkcoordintpair, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, worldserver); // Paper - Anti-Xray - Add parameter + protochunk.setBlockNibbles(blockNibbles); // Tuinity - replace light impl + protochunk.setSkyNibbles(skyNibbles); // Tuinity - replace light impl protochunk.a(biomestorage); object = protochunk; @@ -353,15 +378,20 @@ public class ChunkRegionLoader { NibbleArray[] blockLight = new NibbleArray[17 - (-1)]; NibbleArray[] skyLight = new NibbleArray[17 - (-1)]; + // Tuinity start - rewrite light impl + final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(world); + final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(world); + // Tuinity end - rewrite light impl + for (int i = -1; i < 17; ++i) { - NibbleArray blockArray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i)); - NibbleArray skyArray = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i)); + NibbleArray blockArray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasBlockLight ? null : (chunk.getBlockNibbles()[i - minSection].isAllZero() ? new NibbleArray() : chunk.getBlockNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i)); // Tuinity - chunk might not be loaded + NibbleArray skyArray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasSkyLight ? null : (chunk.getSkyNibbles()[i - minSection].isAllZero() ? new NibbleArray() : chunk.getSkyNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i)); // Tuinity - chunk might not be loaded // copy data for safety - if (blockArray != null) { + if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && blockArray != null) { // Tuinity - data already copied blockArray = blockArray.copy(); } - if (skyArray != null) { + if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && skyArray != null) { // Tuinity - data already copied skyArray = skyArray.copy(); } @@ -396,15 +426,19 @@ public class ChunkRegionLoader { } public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess, AsyncSaveData asyncsavedata) { // Paper end + // Tuinity start - rewrite light impl + final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(worldserver); + final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(worldserver); + // Tuinity end - rewrite light impl ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos(); NBTTagCompound nbttagcompound = new NBTTagCompound(); NBTTagCompound nbttagcompound1 = new NBTTagCompound(); nbttagcompound.setInt("DataVersion", SharedConstants.getGameVersion().getWorldVersion()); - nbttagcompound.set("Level", nbttagcompound1); + nbttagcompound.set("Level", nbttagcompound1); // Tuinity - diff on change nbttagcompound1.setInt("xPos", chunkcoordintpair.x); nbttagcompound1.setInt("zPos", chunkcoordintpair.z); - nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading + nbttagcompound1.setLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : worldserver.getTime()); // Paper - async chunk unloading // Tuinity - diff on change nbttagcompound1.setLong("InhabitedTime", ichunkaccess.getInhabitedTime()); nbttagcompound1.setString("Status", ichunkaccess.getChunkStatus().d()); ChunkConverter chunkconverter = ichunkaccess.p(); @@ -429,8 +463,8 @@ public class ChunkRegionLoader { NibbleArray nibblearray; // block light NibbleArray nibblearray1; // sky light if (asyncsavedata == null) { - nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData) - nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData) + nibblearray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasBlockLight ? null : (ichunkaccess.getBlockNibbles()[i - minSection].isAllZero() ? new NibbleArray() : ichunkaccess.getBlockNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); // Tuinity - chunk might not be loaded + nibblearray1 = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasSkyLight ? null : (ichunkaccess.getSkyNibbles()[i - minSection].isAllZero() ? new NibbleArray() : ichunkaccess.getSkyNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Tuinity - chunk might not be loaded } else { nibblearray = asyncsavedata.blockLight[i + 1]; // +1 to offset the -1 starting index nibblearray1 = asyncsavedata.skyLight[i + 1]; // +1 to offset the -1 starting index @@ -444,12 +478,12 @@ public class ChunkRegionLoader { } if (nibblearray != null && !nibblearray.c()) { - nbttagcompound2.setByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper + nbttagcompound2.setByteArray("BlockLight", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.asBytesPoolSafe().clone()); // Paper // Tuinity - data is already cloned } if (nibblearray1 != null && !nibblearray1.c()) { - nbttagcompound2.setByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper - } + nbttagcompound2.setByteArray("SkyLight", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray1.asBytes() : nibblearray1.asBytesPoolSafe().clone()); // Paper // Tuinity - data is already cloned + } else if (nibblearray1 != null && com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) nbttagcompound2.setBoolean(UNINITIALISED_SKYLIGHT_TAG, true); // Tuinity - store uninitialised tags nbttaglist.add(nbttagcompound2); } @@ -457,7 +491,7 @@ public class ChunkRegionLoader { nbttagcompound1.set("Sections", nbttaglist); if (flag) { - nbttagcompound1.setBoolean("isLightOn", true); + nbttagcompound1.setBoolean("isLightOn", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? false : true); if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) nbttagcompound1.setInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Tuinity } BiomeStorage biomestorage = ichunkaccess.getBiomeIndex(); diff --git a/src/main/java/net/minecraft/server/ChunkSection.java b/src/main/java/net/minecraft/server/ChunkSection.java index e52df8096e399c84ff8a2637fdd65ea57d9001d0..33b8f4e0f09fdc41c8ea48b6ed77af199136ab92 100644 --- a/src/main/java/net/minecraft/server/ChunkSection.java +++ b/src/main/java/net/minecraft/server/ChunkSection.java @@ -11,7 +11,7 @@ public class ChunkSection { short nonEmptyBlockCount; // Paper - package-private short tickingBlockCount; // Paper - private -> package-private private short e; - final DataPaletteBlock blockIds; // Paper - package-private + public final DataPaletteBlock blockIds; // Paper - package-private // Tuinity - public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper @@ -96,6 +96,7 @@ public class ChunkSection { return iblockdata1; } + public final boolean isFullOfAir() { return this.c(); } // Tuinity - OBFHELPER public boolean c() { return this.nonEmptyBlockCount == 0; } diff --git a/src/main/java/net/minecraft/server/ChunkStatus.java b/src/main/java/net/minecraft/server/ChunkStatus.java index f6c9bdbf52d773d7aa601125b887b347163f9328..51ea295d66312c95685b9fe4ee502a029d2fff20 100644 --- a/src/main/java/net/minecraft/server/ChunkStatus.java +++ b/src/main/java/net/minecraft/server/ChunkStatus.java @@ -109,7 +109,7 @@ public class ChunkStatus { private final ChunkStatus.c w; private final int x; private final ChunkStatus.Type y; - private final EnumSet z; + private final EnumSet z; public final HeightMap.Type[] heightMaps; // Tuinity private static CompletableFuture> a(ChunkStatus chunkstatus, LightEngineThreaded lightenginethreaded, IChunkAccess ichunkaccess) { boolean flag = a(chunkstatus, ichunkaccess); @@ -171,7 +171,7 @@ public class ChunkStatus { this.w = chunkstatus_c; this.x = i; this.y = chunkstatus_type; - this.z = enumset; + this.z = enumset; this.heightMaps = new java.util.ArrayList<>(this.z).toArray(new HeightMap.Type[0]); // Tuinity this.t = chunkstatus == null ? 0 : chunkstatus.c() + 1; } diff --git a/src/main/java/net/minecraft/server/DataPaletteBlock.java b/src/main/java/net/minecraft/server/DataPaletteBlock.java index 95ef96286855624590b72d69514b0fc0e08fddba..73163b417af7e522a4509bf9c1ab56d6499be622 100644 --- a/src/main/java/net/minecraft/server/DataPaletteBlock.java +++ b/src/main/java/net/minecraft/server/DataPaletteBlock.java @@ -163,6 +163,7 @@ public class DataPaletteBlock implements DataPaletteExpandable { return this.a(j << 8 | k << 4 | i); // Paper - inline } + public final T rawGet(int index) { return this.a(index); } // Tuinity - OBFHELPER protected T a(int i) { T t0 = this.h.a(this.a.a(i)); diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java index 5504facd2e453238caa71d98743be5416d4dd4fe..ecff0657e5666ddc2e6a5c3111bfb2b8dd2b78d3 100644 --- a/src/main/java/net/minecraft/server/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/DedicatedServer.java @@ -169,6 +169,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer com.destroystokyo.paper.PaperConfig.registerCommands(); com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // load version history now // Paper end + com.tuinity.tuinity.config.TuinityConfig.init((java.io.File) options.valueOf("tuinity-settings")); // Tuinity - Server Config this.setPVP(dedicatedserverproperties.pvp); this.setAllowFlight(dedicatedserverproperties.allowFlight); @@ -357,7 +358,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer } if (this.q != null) { - this.q.b(); + //this.q.b(); // Tuinity - do not wait for AWT, causes deadlock with sigint handler (AWT shutdown will properly clear our resources anyways) } if (this.remoteControlListener != null) { diff --git a/src/main/java/net/minecraft/server/EULA.java b/src/main/java/net/minecraft/server/EULA.java index 550232cb3819138b3bae0fa1c51429485e8bc593..229c3b0f0c650b501f31147adaa17194af57fedd 100644 --- a/src/main/java/net/minecraft/server/EULA.java +++ b/src/main/java/net/minecraft/server/EULA.java @@ -70,7 +70,7 @@ public class EULA { Properties properties = new Properties(); properties.setProperty("eula", "false"); - properties.store(outputstream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\nYou also agree that tacos are tasty, and the best food in the world."); // Paper - fix lag; + properties.store(outputstream, "By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula)."); // Paper - fix lag; // Tuinity - Tacos are disgusting } catch (Throwable throwable1) { throwable = throwable1; throw throwable1; diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java index f307a6361144c7e315b2e0ea45df27527cdb26ca..f292e15746a947c580aa93e0a23dbc032eccb561 100644 --- a/src/main/java/net/minecraft/server/Entity.java +++ b/src/main/java/net/minecraft/server/Entity.java @@ -136,7 +136,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke public double D; public double E; public double F; - public float G; + public float G; public final float getStepHeight() { return this.G; } // Tuinity - OBFHELPER public boolean noclip; public float I; protected final Random random; @@ -207,6 +207,14 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } // CraftBukkit end + // Tuinity start + public final AxisAlignedBB getBoundingBoxAt(double x, double y, double z) { + double widthHalf = (double)this.size.width / 2.0; + double height = (double)this.size.height; + return new AxisAlignedBB(x - widthHalf, y, z - widthHalf, x + widthHalf, y + height, z + widthHalf); + } + // Tuinity end + // Paper start - optimise entity tracking final org.spigotmc.TrackingRange.TrackingRangeType trackingRangeType = org.spigotmc.TrackingRange.getTrackingRangeType(this); @@ -222,6 +230,41 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } // Paper end - optimise entity tracking + // Tuinity start + /** + * Overriding this field will cause memory leaks. + */ + private final boolean hardCollides; + + private static final java.util.Map, Boolean> cachedOverrides = java.util.Collections.synchronizedMap(new java.util.WeakHashMap<>()); + { + Boolean hardCollides = cachedOverrides.get(this.getClass()); + if (hardCollides == null) { + try { + java.lang.reflect.Method getHardCollisionBoxEntityMethod = Entity.class.getMethod("j", Entity.class); + java.lang.reflect.Method hasHardCollisionBoxMethod = Entity.class.getMethod("aZ"); + if (!this.getClass().getMethod(hasHardCollisionBoxMethod.getName(), hasHardCollisionBoxMethod.getParameterTypes()).equals(hasHardCollisionBoxMethod) + || !this.getClass().getMethod(getHardCollisionBoxEntityMethod.getName(), getHardCollisionBoxEntityMethod.getParameterTypes()).equals(getHardCollisionBoxEntityMethod)) { + hardCollides = Boolean.TRUE; + } else { + hardCollides = Boolean.FALSE; + } + cachedOverrides.put(this.getClass(), hardCollides); + } + catch (ThreadDeath thr) { throw thr; } + catch (Throwable thr) { + // shouldn't happen, just explode + throw new RuntimeException(thr); + } + } + this.hardCollides = hardCollides.booleanValue(); + } + + public final boolean hardCollides() { + return this.hardCollides; + } + // Tuinity end + public Entity(EntityTypes entitytypes, World world) { this.id = Entity.entityCount.incrementAndGet(); this.passengers = Lists.newArrayList(); @@ -591,7 +634,39 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke return this.onGround; } + // Tuinity start - detailed watchdog information + private Vec3D moveVector; + private double moveStartX; + private double moveStartY; + private double moveStartZ; + + public final Vec3D getMoveVector() { + return this.moveVector; + } + + public final double getMoveStartX() { + return this.moveStartX; + } + + public final double getMoveStartY() { + return this.moveStartY; + } + + public final double getMoveStartZ() { + return this.moveStartZ; + } + // Tuinity end - detailed watchdog information public void move(EnumMoveType enummovetype, Vec3D vec3d) { + // Tuinity start - detailed watchdog information + com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot move an entity off-main"); + synchronized (this.posLock) { + this.moveStartX = this.locX(); + this.moveStartY = this.locY(); + this.moveStartZ = this.locZ(); + this.moveVector = vec3d; + } + try { + // Tuinity end - detailed watchdog information if (this.noclip) { this.a(this.getBoundingBox().c(vec3d)); this.recalcPosition(); @@ -619,7 +694,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke // Paper end vec3d = this.a(vec3d, enummovetype); - Vec3D vec3d1 = this.g(vec3d); + Vec3D vec3d1 = this.performCollision(vec3d); // Tuinity - optimise collisions if (vec3d1.g() > 1.0E-7D) { this.a(this.getBoundingBox().c(vec3d1)); @@ -710,7 +785,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } try { - this.checkBlockCollisions(); + this.checkBlockCollisions(this.fireTicks <= 0); // Tuinity - move fire checking into method here } catch (Throwable throwable) { CrashReport crashreport = CrashReport.a(throwable, "Checking entity block collision"); CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Entity being checked for collision"); @@ -722,11 +797,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke float f2 = this.getBlockSpeedFactor(); this.setMot(this.getMot().d((double) f2, 1.0D, (double) f2)); - if (this.world.c(this.getBoundingBox().shrink(0.001D)).noneMatch((iblockdata1) -> { - return iblockdata1.a((Tag) TagsBlock.FIRE) || iblockdata1.a(Blocks.LAVA); - }) && this.fireTicks <= 0) { - this.setFireTicks(-this.getMaxFireTicks()); - } + // Tuinity - move into checkBlockCollisions if (this.aG() && this.isBurning()) { this.playSound(SoundEffects.ENTITY_GENERIC_EXTINGUISH_FIRE, 0.7F, 1.6F + (this.random.nextFloat() - this.random.nextFloat()) * 0.4F); @@ -735,6 +806,13 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke this.world.getMethodProfiler().exit(); } + // Tuinity start - detailed watchdog information + } finally { + synchronized (this.posLock) { // Tuinity + this.moveVector = null; + } // Tuinity + } + // Tuinity end - detailed watchdog information } protected BlockPosition ap() { @@ -815,6 +893,146 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke return d0; } + // Tuinity start - optimise entity movement + private static double performCollisionsX(AxisAlignedBB currentBoundingBox, double value, List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < MCUtil.COLLISION_EPSILON) { + return 0.0; + } + AxisAlignedBB target = potentialCollisions.get(i); + value = AxisAlignedBB.collideX(target, currentBoundingBox, value); + } + + return value; + } + + private static double performCollisionsY(AxisAlignedBB currentBoundingBox, double value, List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < MCUtil.COLLISION_EPSILON) { + return 0.0; + } + AxisAlignedBB target = potentialCollisions.get(i); + value = AxisAlignedBB.collideY(target, currentBoundingBox, value); + } + + return value; + } + + private static double performCollisionsZ(AxisAlignedBB currentBoundingBox, double value, List potentialCollisions) { + for (int i = 0, len = potentialCollisions.size(); i < len; ++i) { + if (Math.abs(value) < MCUtil.COLLISION_EPSILON) { + return 0.0; + } + AxisAlignedBB target = potentialCollisions.get(i); + value = AxisAlignedBB.collideZ(target, currentBoundingBox, value); + } + + return value; + } + + private static Vec3D performCollisions(Vec3D moveVector, AxisAlignedBB axisalignedbb, List potentialCollisions) { + double x = moveVector.x; + double y = moveVector.y; + double z = moveVector.z; + + if (y != 0.0) { + y = Entity.performCollisionsY(axisalignedbb, y, potentialCollisions); + if (y != 0.0) { + axisalignedbb = axisalignedbb.offsetY(y); + } + } + + boolean xSmaller = Math.abs(x) < Math.abs(z); + + if (xSmaller && z != 0.0) { + z = Entity.performCollisionsZ(axisalignedbb, z, potentialCollisions); + if (z != 0.0) { + axisalignedbb = axisalignedbb.offsetZ(z); + } + } + + if (x != 0.0) { + x = Entity.performCollisionsX(axisalignedbb, x, potentialCollisions); + if (!xSmaller && x != 0.0) { + axisalignedbb = axisalignedbb.offsetX(x); + } + } + + if (!xSmaller && z != 0.0) { + z = Entity.performCollisionsZ(axisalignedbb, z, potentialCollisions); + } + + return new Vec3D(x, y, z); + } + + Vec3D performCollision(Vec3D moveVector) { + if (moveVector.getX() == 0.0 && moveVector.getY() == 0.0 && moveVector.getZ() == 0.0) { + return moveVector; + } + + WorldServer world = ((WorldServer)this.world); + AxisAlignedBB currBoundingBox = this.getBoundingBox(); + + List potentialCollisions = com.tuinity.tuinity.util.CachedLists.getTempCollisionList(); + try { + AxisAlignedBB collisionBox; + double stepHeight = (double)this.getStepHeight(); + + if (moveVector.x == 0.0 && moveVector.z == 0.0 && moveVector.y != 0.0) { + // a lot of entities just stand still. optimise the search AABB + if (moveVector.y > 0.0) { + collisionBox = currBoundingBox.cutUpwards(moveVector.y); + } else { + collisionBox = currBoundingBox.cutDownwards(moveVector.y); + } + } else { + if (stepHeight > 0.0 && (this.onGround || (moveVector.y < 0.0)) && (moveVector.x != 0.0 || moveVector.z != 0.0)) { + // don't bother getting the collisions if we don't need them. + if (moveVector.y <= 0.0) { + collisionBox = currBoundingBox.expand(moveVector.x, moveVector.y, moveVector.z).expandUpwards(stepHeight); + } else { + collisionBox = currBoundingBox.expand(moveVector.x, Math.max(stepHeight, moveVector.y), moveVector.z); + } + } else { + collisionBox = currBoundingBox.expand(moveVector.x, moveVector.y, moveVector.z); + } + } + + world.getCollisions(this, collisionBox, potentialCollisions, this instanceof EntityPlayer && !this.world.paperConfig.preventMovingIntoUnloadedChunks); + if (world.getWorldBorder().isCollidingWithBorderEdge(collisionBox)) { + VoxelShapes.addBoxesToIfIntersects(world.getWorldBorder().getCollisionShape(), collisionBox, potentialCollisions); + } + + Vec3D limitedMoveVector = Entity.performCollisions(moveVector, currBoundingBox, potentialCollisions); + + if (stepHeight > 0.0 + && (this.onGround || (limitedMoveVector.y != moveVector.y && moveVector.y < 0.0)) + && (limitedMoveVector.x != moveVector.x || limitedMoveVector.z != moveVector.z)) { + Vec3D vec3d2 = Entity.performCollisions(new Vec3D(moveVector.x, stepHeight, moveVector.z), currBoundingBox, potentialCollisions); + Vec3D vec3d3 = Entity.performCollisions(new Vec3D(0.0, stepHeight, 0.0), currBoundingBox.expand(moveVector.x, 0.0, moveVector.z), potentialCollisions); + + if (vec3d3.y < stepHeight) { + Vec3D vec3d4 = Entity.performCollisions(new Vec3D(moveVector.x, 0.0D, moveVector.z), currBoundingBox.offset(vec3d3), potentialCollisions).add(vec3d3); + + if (Entity.getXZSquared(vec3d4) > Entity.getXZSquared(vec3d2)) { + vec3d2 = vec3d4; + } + } + + if (Entity.getXZSquared(vec3d2) > Entity.getXZSquared(limitedMoveVector)) { + return vec3d2.add(Entity.performCollisions(new Vec3D(0.0D, -vec3d2.y + moveVector.y, 0.0D), currBoundingBox.offset(vec3d2), potentialCollisions)); + } + + return limitedMoveVector; + } else { + return limitedMoveVector; + } + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempCollisionList(potentialCollisions); + } + } + // Tuinity end - optimise entity movement + private Vec3D g(Vec3D vec3d) { AxisAlignedBB axisalignedbb = this.getBoundingBox(); VoxelShapeCollision voxelshapecollision = VoxelShapeCollision.a(this); @@ -850,6 +1068,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke return vec3d1; } + public static double getXZSquared(Vec3D vec3d) { return Entity.c(vec3d); } // Tuinity - OBFHELPER public static double c(Vec3D vec3d) { return vec3d.x * vec3d.x + vec3d.z * vec3d.z; } @@ -962,18 +1181,34 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } protected void checkBlockCollisions() { + // Tuinity start + this.checkBlockCollisions(false); + } + protected void checkBlockCollisions(boolean checkFire) { + boolean inFire = false; + // Tuinity end AxisAlignedBB axisalignedbb = this.getBoundingBox(); BlockPosition blockposition = new BlockPosition(axisalignedbb.minX + 0.001D, axisalignedbb.minY + 0.001D, axisalignedbb.minZ + 0.001D); BlockPosition blockposition1 = new BlockPosition(axisalignedbb.maxX - 0.001D, axisalignedbb.maxY - 0.001D, axisalignedbb.maxZ - 0.001D); BlockPosition.MutableBlockPosition blockposition_mutableblockposition = new BlockPosition.MutableBlockPosition(); if (this.world.areChunksLoadedBetween(blockposition, blockposition1)) { - for (int i = blockposition.getX(); i <= blockposition1.getX(); ++i) { - for (int j = blockposition.getY(); j <= blockposition1.getY(); ++j) { - for (int k = blockposition.getZ(); k <= blockposition1.getZ(); ++k) { + // Tuinity start - reorder iteration to more cache aware + for (int j = blockposition.getY(); j <= blockposition1.getY(); ++j) { + for (int k = blockposition.getZ(); k <= blockposition1.getZ(); ++k) { + for (int i = blockposition.getX(); i <= blockposition1.getX(); ++i) { + // Tuinity end - reorder iteration to more cache aware blockposition_mutableblockposition.d(i, j, k); IBlockData iblockdata = this.world.getType(blockposition_mutableblockposition); + // Tuinity start - move fire checking in here - reuse getType from this method + if (checkFire) { + if (!inFire && (iblockdata.a(TagsBlock.FIRE) || iblockdata.a(Blocks.LAVA))) { + inFire = true; + } + } + // Tuinity end - move fire checking in here - reuse getType from this method + try { iblockdata.a(this.world, blockposition_mutableblockposition, this); this.a(iblockdata); @@ -987,6 +1222,11 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } } } + // Tuinity start - move fire checking in here - reuse getType from this method + if (checkFire & !inFire) { + this.setFireTicks(-this.getMaxFireTicks()); + } + // Tuinity end - move fire checking in here - reuse getType from this method } } @@ -1358,6 +1598,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke return d3 * d3 + d4 * d4 + d5 * d5; } + public final double getDistanceSquared(Entity other) { return this.h(other); } // Tuinity - OBFHELPER public double h(Entity entity) { return this.e(entity.getPositionVector()); } @@ -1945,9 +2186,9 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke float f1 = this.size.width * 0.8F; AxisAlignedBB axisalignedbb = AxisAlignedBB.g((double) f1, 0.10000000149011612D, (double) f1).d(this.locX(), this.getHeadY(), this.locZ()); - return this.world.b(this, axisalignedbb, (iblockdata, blockposition) -> { + return ((WorldServer)this.world).collidesWithAnyBlockOrWorldBorder(this, axisalignedbb, false, false, (iblockdata, blockposition) -> { // Tuinity - use optimised method return iblockdata.o(this.world, blockposition); - }).findAny().isPresent(); + }); // Tuinity - use optimised method } } @@ -1955,11 +2196,13 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke return EnumInteractionResult.PASS; } - public boolean j(Entity entity) { + public final boolean hardCollidesWith(Entity other) { return this.j(other); } // Tuinity - OBFHELPER + public boolean j(Entity entity) { // Tuinity - diff on change, hard colliding entities override this return entity.aZ() && !this.isSameVehicle(entity); } - public boolean aZ() { + public final boolean collisionBoxIsHard() { return this.aZ(); } // Tuinity - OBFHELPER + public boolean aZ() {// Tuinity - diff on change, hard colliding entities override this return false; } @@ -2850,7 +3093,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke this.recursiveStream().forEach((entity) -> { worldserver.chunkCheck(entity); entity.az = true; - Iterator iterator = entity.passengers.iterator(); + Iterator iterator = new java.util.ArrayList<>(entity.passengers).iterator(); // Tuinity - copy list to guard against CME while (iterator.hasNext()) { Entity entity1 = (Entity) iterator.next(); @@ -3308,12 +3551,16 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke return this.locBlock; } + public final Object posLock = new Object(); // Tuinity - log detailed entity tick information + public Vec3D getMot() { return this.mot; } public void setMot(Vec3D vec3d) { + synchronized (this.posLock) { // Tuinity this.mot = vec3d; + } // Tuinity } public void setMot(double d0, double d1, double d2) { @@ -3368,7 +3615,9 @@ public abstract class Entity implements INamableTileEntity, ICommandListener, Ke } // Paper end if (this.loc.x != d0 || this.loc.y != d1 || this.loc.z != d2) { + synchronized (this.posLock) { // Tuinity this.loc = new Vec3D(d0, d1, d2); + } // Tuinity int i = MathHelper.floor(d0); int j = MathHelper.floor(d1); int k = MathHelper.floor(d2); diff --git a/src/main/java/net/minecraft/server/EntityCat.java b/src/main/java/net/minecraft/server/EntityCat.java index 957a351c3f6c4f66d7af6657ab0c3cbeed94662f..57166a543a9af9e10e38c983487fac7ea9d42d52 100644 --- a/src/main/java/net/minecraft/server/EntityCat.java +++ b/src/main/java/net/minecraft/server/EntityCat.java @@ -292,7 +292,7 @@ public class EntityCat extends EntityTameableAnimal { WorldServer worldserver = worldaccess.getMinecraftWorld(); - if (worldserver instanceof WorldServer && ((WorldServer) worldserver).getStructureManager().a(this.getChunkCoordinates(), true, StructureGenerator.SWAMP_HUT).e()) { + if (worldserver instanceof WorldServer && ((WorldServer) worldserver).getStructureManager().getStructureStarts(this.getChunkCoordinates(), true, StructureGenerator.SWAMP_HUT, worldaccess).e()) { // Tuinity - fix deadlock on chunk gen this.setCatType(10); this.setPersistent(); } diff --git a/src/main/java/net/minecraft/server/EntityInsentient.java b/src/main/java/net/minecraft/server/EntityInsentient.java index eb5c3a1f0d9ff665631caf5bf579e83d1ed25e4f..7582a3a0955db2bc79daeced8e9c869f4276815a 100644 --- a/src/main/java/net/minecraft/server/EntityInsentient.java +++ b/src/main/java/net/minecraft/server/EntityInsentient.java @@ -710,7 +710,13 @@ public abstract class EntityInsentient extends EntityLiving { if (this.world.getDifficulty() == EnumDifficulty.PEACEFUL && this.L()) { this.die(); } else if (!this.isPersistent() && !this.isSpecialPersistence()) { - EntityHuman entityhuman = this.world.findNearbyPlayer(this, -1.0D, IEntitySelector.affectsSpawning); // Paper + // Tuinity start - optimise checkDespawn + Chunk chunk = this.getCurrentChunk(); + EntityHuman entityhuman = chunk == null || this.world.paperConfig.hardDespawnDistance >= (31 * 16 * 31 * 16) ? this.world.findNearbyPlayer(this, -1.0D, IEntitySelector.affectsSpawning) : chunk.findNearestPlayer(this, IEntitySelector.affectsSpawning); // Paper + if (entityhuman == null) { + entityhuman = ((WorldServer)this.world).playersAffectingSpawning.isEmpty() ? null : ((WorldServer)this.world).playersAffectingSpawning.get(0); + } + // Tuinity end - optimise checkDespawn if (entityhuman != null) { double d0 = entityhuman.h((Entity) this); // CraftBukkit - decompile error diff --git a/src/main/java/net/minecraft/server/EntityItem.java b/src/main/java/net/minecraft/server/EntityItem.java index f41aaa7623c052b9f4044898d1bdee898c03057a..d99cecc4075338d7b8f154ab94d8ac04190ba371 100644 --- a/src/main/java/net/minecraft/server/EntityItem.java +++ b/src/main/java/net/minecraft/server/EntityItem.java @@ -526,7 +526,7 @@ public class EntityItem extends Entity { // Paper start - fix MC-4 public void setPositionRaw(double x, double y, double z) { - if (com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { + if (false && com.destroystokyo.paper.PaperConfig.fixEntityPositionDesync) { // Tuinity - revert // encode/decode from PacketPlayOutEntity x = MathHelper.floorLong(x * 4096.0D) * (1 / 4096.0D); y = MathHelper.floorLong(y * 4096.0D) * (1 / 4096.0D); diff --git a/src/main/java/net/minecraft/server/EntityLiving.java b/src/main/java/net/minecraft/server/EntityLiving.java index d417878a1584c9e5ec33b94fc65b29f84fb3a5e9..4fe5a8d0201d662c68dd58eeb8cf1d304787edb4 100644 --- a/src/main/java/net/minecraft/server/EntityLiving.java +++ b/src/main/java/net/minecraft/server/EntityLiving.java @@ -2858,7 +2858,11 @@ public abstract class EntityLiving extends Entity { return; } // Paper - end don't run getEntities if we're not going to use its result - List list = this.world.getEntities(this, this.getBoundingBox(), IEntitySelector.pushable(this, world.paperConfig.fixClimbingBypassingCrammingRule)); // Paper - fix climbing bypassing cramming rule + // Tuinity start - reduce memory allocation from collideNearby + List list = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList(); + this.world.getEntities(this, this.getBoundingBox(), IEntitySelector.pushable(this, world.paperConfig.fixClimbingBypassingCrammingRule), list); // Paper - fix climbing bypassing cramming rule + try { + // Tuinity end - reduce memory allocation from collideNearby if (!list.isEmpty()) { // Paper - move up @@ -2887,6 +2891,9 @@ public abstract class EntityLiving extends Entity { this.C(entity); } } + } finally { // Tuinity start - reduce memory allocation from collideNearby + com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(list); + } // Tuinity end - reduce memory allocation from collideNearby } diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java index 04d505c7a19775d0353c10424a84a1ce8885dc2c..9f5b7243ccbe0729a061345c25033d9145b91b3f 100644 --- a/src/main/java/net/minecraft/server/EntityPlayer.java +++ b/src/main/java/net/minecraft/server/EntityPlayer.java @@ -527,6 +527,185 @@ public class EntityPlayer extends EntityHuman implements ICrafting { } } + /* // TODO remove debug + this.networkManager.disableAutomaticFlush(); + + if (MinecraftServer.currentTick % 20 == 0) { + int centerX = MathHelper.floor(this.locX()) >> 4; + int centerZ = MathHelper.floor(this.locZ()) >> 4; + byte[] full = new byte[2048]; + byte[] empty = new byte[2048]; + java.util.Arrays.fill(full, (byte)-1); + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + int cx = centerX + dx; + int cz = centerZ + dz; + + Chunk chunk = this.getWorldServer().getChunkProvider().getChunkAtIfLoadedImmediately(cx, cz); + + if (chunk == null) { + continue; + } + + for (int y = -1; y <= 16; ++y) { + NibbleArray nibble = this.getWorldServer().getChunkProvider().getLightEngine().a(EnumSkyBlock.SKY) + .a(new SectionPosition(cx, y, cz)); + org.bukkit.Color color; + org.bukkit.block.data.BlockData blockColor; + if (nibble == null) { + color = org.bukkit.Color.PURPLE; + blockColor = org.bukkit.Material.PURPLE_WOOL.createBlockData(); + continue; + } else { + if (nibble.c()) { // is null + color = org.bukkit.Color.BLUE; + blockColor = org.bukkit.Material.BLUE_WOOL.createBlockData(); + } else if (java.util.Arrays.equals(nibble.justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist(), full)) { + color = org.bukkit.Color.RED; + blockColor = org.bukkit.Material.RED_WOOL.createBlockData(); + } else if (java.util.Arrays.equals(nibble.justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist(), empty)) { + color = org.bukkit.Color.BLACK; + blockColor = org.bukkit.Material.BLACK_WOOL.createBlockData(); + } else { + color = org.bukkit.Color.ORANGE; + blockColor = org.bukkit.Material.ORANGE_WOOL.createBlockData(); + } + } + if (false) { + if (y < 0 || y > 15 || chunk.getSections()[y] == null || chunk.getSections()[y].isFullOfAir()) { + color = org.bukkit.Color.BLACK; + blockColor = org.bukkit.Material.BLACK_WOOL.createBlockData(); + } else { + color = org.bukkit.Color.WHITE; + blockColor = org.bukkit.Material.WHITE_WOOL.createBlockData(); + } + } + + org.bukkit.Particle.DustOptions dustOptions = new org.bukkit.Particle.DustOptions(color, 1.7f); + + for (int i = 0; i <= 16; ++i) { + // y axis + + double xVal = i == 0 ? 0.5 : (i == 16 ? 15.5 : i); + + // left side + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 0.5, + y*16 + xVal, + cz * 16 + 0.5, + 1, + dustOptions + ); + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 0.5, + y*16 + xVal, + cz * 16 + 15.5, + 1, + dustOptions + ); + + // right side + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 15.5, + y*16 + xVal, + cz * 16 + 0.5, + 1, + dustOptions + ); + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 15.5, + y*16 + xVal, + cz * 16 + 15.5, + 1, + dustOptions + ); + + + // x axis + + // bottom + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + xVal, + y*16 + 0.5, + cz * 16 + 0.5, + 1, + dustOptions + ); + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + xVal, + y*16 + 0.5, + cz * 16 + 15.5, + 1, + dustOptions + ); + + // top + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + xVal, + y*16 + 15.5, + cz * 16 + 0.5, + 1, + dustOptions + ); + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + xVal, + y*16 + 15.5, + cz * 16 + 15.5, + 1, + dustOptions + ); + + // z axis + // bottom + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 0.5, + y*16 + 0.5, + cz * 16 + xVal, + 1, + dustOptions + ); + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 15.5, + y*16 + 0.5, + cz * 16 + xVal, + 1, + dustOptions + ); + + //top + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 0.5, + y*16 + 15.5, + cz * 16 + xVal, + 1, + dustOptions + ); + + this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE, + cx * 16 + 15.5, + y*16 + 15.5, + cz * 16 + xVal, + 1, + dustOptions + ); + } + } + } + } + } + + this.networkManager.enableAutomaticFlush(); + + //System.out.println("Block: " + this.getBukkitEntity().getLocation().getBlock().getLightFromBlocks()); + //System.out.println("Sky: " + this.getBukkitEntity().getLocation().getBlock().getLightFromSky()); + */ // TODO remove debug + if (this.getHealth() != this.lastHealthSent || this.lastFoodSent != this.foodData.getFoodLevel() || this.foodData.getSaturationLevel() == 0.0F != this.lastSentSaturationZero) { this.playerConnection.sendPacket(new PacketPlayOutUpdateHealth(this.getBukkitEntity().getScaledHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel())); // CraftBukkit this.lastHealthSent = this.getHealth(); diff --git a/src/main/java/net/minecraft/server/EntityTrackerEntry.java b/src/main/java/net/minecraft/server/EntityTrackerEntry.java index 4efc40c01ec12b80bd7cf9d35cf0ea0df973baf7..f322dccd834ff56b99f8796309709b5b6ac01456 100644 --- a/src/main/java/net/minecraft/server/EntityTrackerEntry.java +++ b/src/main/java/net/minecraft/server/EntityTrackerEntry.java @@ -74,6 +74,7 @@ public class EntityTrackerEntry { public final void tick() { this.a(); } // Paper - OBFHELPER public void a() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Tracker update"); // Tuinity List list = this.tracker.getPassengers(); if (!list.equals(this.p)) { diff --git a/src/main/java/net/minecraft/server/EnumDirection.java b/src/main/java/net/minecraft/server/EnumDirection.java index 1aa070db60f5473576fb5d056cadde5106766489..24e6f3141ff4434f770e956a8d240bf856442933 100644 --- a/src/main/java/net/minecraft/server/EnumDirection.java +++ b/src/main/java/net/minecraft/server/EnumDirection.java @@ -160,8 +160,8 @@ public enum EnumDirection implements INamable { return EnumDirection.q[MathHelper.a(i % EnumDirection.q.length)]; } - @Nullable - public static EnumDirection a(int i, int j, int k) { + @Nullable public static EnumDirection from(int i, int j, int k) { return a(i, j, k); } // Tuinity - OBFHELPER + @Nullable public static EnumDirection a(int i, int j, int k) { return (EnumDirection) EnumDirection.r.get(BlockPosition.a(i, j, k)); } diff --git a/src/main/java/net/minecraft/server/HeightMap.java b/src/main/java/net/minecraft/server/HeightMap.java index 068b92c5c4ae112771757626ea75694e59f3d255..476da43b9f0ef35b4985f88e4784b1f8c5222af3 100644 --- a/src/main/java/net/minecraft/server/HeightMap.java +++ b/src/main/java/net/minecraft/server/HeightMap.java @@ -101,6 +101,7 @@ public class HeightMap { } } + public final int get(int x, int z) { return this.a(x, z); } // Tuinity - OBFHELPER public int a(int i, int j) { return this.a(c(i, j)); } @@ -137,7 +138,7 @@ public class HeightMap { private final String h; private final HeightMap.Use i; private final Predicate j; - private static final Map k = (Map) SystemUtils.a((Object) Maps.newHashMap(), (hashmap) -> { + private static final Map k = (Map) SystemUtils.a(Maps.newHashMap(), (hashmap) -> { // Tuinity - decompile fix HeightMap.Type[] aheightmap_type = values(); int i = aheightmap_type.length; @@ -149,7 +150,7 @@ public class HeightMap { }); - private Type(String s, HeightMap.Use heightmap_use, Predicate predicate) { + private Type(String s, HeightMap.Use heightmap_use, Predicate predicate) { // Tuinity - decompile fix this.h = s; this.i = heightmap_use; this.j = predicate; diff --git a/src/main/java/net/minecraft/server/IBlockAccess.java b/src/main/java/net/minecraft/server/IBlockAccess.java index c4a83448ed4513f6e4ab179d1d43e5bb0cb13641..5c3eb4fc7e5aec2ad8d0050673fc8f4d2bff6a71 100644 --- a/src/main/java/net/minecraft/server/IBlockAccess.java +++ b/src/main/java/net/minecraft/server/IBlockAccess.java @@ -55,7 +55,7 @@ public interface IBlockAccess { return MovingObjectPositionBlock.a(raytrace1.a(), EnumDirection.a(vec3d.x, vec3d.y, vec3d.z), new BlockPosition(raytrace1.a())); } // Paper end - Fluid fluid = this.getFluid(blockposition); + Fluid fluid = iblockdata.getFluid(); // Tuinity - don't need to go to world state again Vec3D vec3d = raytrace1.b(); Vec3D vec3d1 = raytrace1.a(); VoxelShape voxelshape = raytrace1.a(iblockdata, this, blockposition); diff --git a/src/main/java/net/minecraft/server/IChunkAccess.java b/src/main/java/net/minecraft/server/IChunkAccess.java index 180b6b58dc5663158db84b6f1257591439b48c31..62a648a78481b4f3f35882c689e137e733f7f152 100644 --- a/src/main/java/net/minecraft/server/IChunkAccess.java +++ b/src/main/java/net/minecraft/server/IChunkAccess.java @@ -24,6 +24,28 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess { } // Paper end + // Tuinity start + default com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + default void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + + default com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + default void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + public default boolean[] getEmptinessMap() { + throw new UnsupportedOperationException(this.getClass().getName()); + } + public default void setEmptinessMap(final boolean[] emptinessMap) { + throw new UnsupportedOperationException(this.getClass().getName()); + } + // Tuinity end + IBlockData getType(final int x, final int y, final int z); // Paper @Nullable IBlockData setType(BlockPosition blockposition, IBlockData iblockdata, boolean flag); @@ -122,6 +144,7 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess { @Nullable NBTTagCompound j(BlockPosition blockposition); + default Stream getLightSources() { return this.m(); } // Tuinity - OBFHELPER Stream m(); TickList n(); @@ -142,6 +165,7 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess { return ashortlist[i]; } + default boolean isLit() { return this.r(); } // Tuinity - OBFHELPER boolean r(); void b(boolean flag); diff --git a/src/main/java/net/minecraft/server/IChunkLoader.java b/src/main/java/net/minecraft/server/IChunkLoader.java index 582a5695bac7d078e3022b8ee70c512c0680d992..5601088cd5024a40e8296bab979f43de924c2b62 100644 --- a/src/main/java/net/minecraft/server/IChunkLoader.java +++ b/src/main/java/net/minecraft/server/IChunkLoader.java @@ -21,7 +21,7 @@ public class IChunkLoader implements AutoCloseable { protected final RegionFileCache regionFileCache; public IChunkLoader(File file, DataFixer datafixer, boolean flag) { - this.regionFileCache = new RegionFileCache(file, flag); // Paper - nuke IOWorker + this.regionFileCache = new RegionFileCache(file, flag, true); // Paper - nuke IOWorker // Tuinity this.b = datafixer; // Paper - nuke IOWorker } diff --git a/src/main/java/net/minecraft/server/ICollisionAccess.java b/src/main/java/net/minecraft/server/ICollisionAccess.java index 25e54a1fadc5d31fb250a3f47524b4f345fc8cc6..cce0ac8a36bef3b9e5a2b95e0c3dd137e8525226 100644 --- a/src/main/java/net/minecraft/server/ICollisionAccess.java +++ b/src/main/java/net/minecraft/server/ICollisionAccess.java @@ -28,6 +28,11 @@ public interface ICollisionAccess extends IBlockAccess { } default boolean b(AxisAlignedBB axisalignedbb) { + // Tuinity start - allow overriding in WorldServer + return this.getCubes(axisalignedbb); + } + default boolean getCubes(AxisAlignedBB axisalignedbb) { + // Tuinity end - allow overriding in WorldServer return this.b((Entity) null, axisalignedbb, (entity) -> { return true; }); @@ -46,6 +51,11 @@ public interface ICollisionAccess extends IBlockAccess { } default boolean b(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + // Tuinity start - allow overriding in WorldServer + return this.getCubes(entity, axisalignedbb, predicate); + } + default boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + // Tuinity end - allow overriding in WorldServer try { if (entity != null) entity.collisionLoadChunks = true; // Paper return this.d(entity, axisalignedbb, predicate).allMatch(VoxelShape::isEmpty); } finally { if (entity != null) entity.collisionLoadChunks = false; } // Paper diff --git a/src/main/java/net/minecraft/server/IEntityAccess.java b/src/main/java/net/minecraft/server/IEntityAccess.java index 2639c17b7f6100533f33124f9e49990cd303d161..cbaf18af1066e8bde10293bba5eb3060bae1e66f 100644 --- a/src/main/java/net/minecraft/server/IEntityAccess.java +++ b/src/main/java/net/minecraft/server/IEntityAccess.java @@ -55,16 +55,26 @@ public interface IEntityAccess { return this.b(oclass, axisalignedbb, IEntitySelector.g); } + // Tuinity start - optimise hard collision + /** + * Not guaranteed to only return hard colliding entities + */ + default List getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + return this.getEntities(entity, axisalignedbb, predicate); + } + // Tuinity end - optimise hard collision + default Stream c(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { if (axisalignedbb.a() < 1.0E-7D) { return Stream.empty(); } else { AxisAlignedBB axisalignedbb1 = axisalignedbb.g(1.0E-7D); - return this.getEntities(entity, axisalignedbb1, predicate.and((entity1) -> { + if (predicate == null) predicate = (e) -> true; // Tuinity - allow nullable + predicate = predicate.and((entity1) -> { // Tuinity - optimise entity hard collisions // Tuinity - allow nullable boolean flag; - if (entity1.getBoundingBox().c(axisalignedbb1)) { + if (true || entity1.getBoundingBox().c(axisalignedbb1)) { // Tuinity - always true, wtf did they think this.getEntities(entity, axisalignedbb1) does? label25: { if (entity == null) { @@ -82,7 +92,7 @@ public interface IEntityAccess { flag = false; return flag; - })).stream().map(Entity::getBoundingBox).map(VoxelShapes::a); + }); return ((entity != null && entity.hardCollides()) ? this.getEntities(entity, axisalignedbb1, predicate) : this.getHardCollidingEntities(entity, axisalignedbb1, predicate)).stream().map(Entity::getBoundingBox).map(VoxelShapes::a); // Tuinity - optimise entity hard collisions } } @@ -204,12 +214,12 @@ public interface IEntityAccess { } @Nullable - default T a(Class oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { + default T a(Class oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get closest entity by class that matches path finder target condition" return this.a(this.a(oclass, axisalignedbb, null), pathfindertargetcondition, entityliving, d0, d1, d2); // Paper - decompile fix } @Nullable - default T b(Class oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { + default T b(Class oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get closest entity by class that matches path finder target condition" return this.a(this.b(oclass, axisalignedbb, null), pathfindertargetcondition, entityliving, d0, d1, d2); // Paper - decompile fix } diff --git a/src/main/java/net/minecraft/server/ILightAccess.java b/src/main/java/net/minecraft/server/ILightAccess.java index be5384ee41290b24b0c419c3e8f4553db34b2399..df28f7a6bf4c650a22ddf046eae4d5e8ca5879a9 100644 --- a/src/main/java/net/minecraft/server/ILightAccess.java +++ b/src/main/java/net/minecraft/server/ILightAccess.java @@ -4,9 +4,10 @@ import javax.annotation.Nullable; public interface ILightAccess { - @Nullable - IBlockAccess c(int i, int j); + default @Nullable IBlockAccess getFeaturesReadyChunk(int i, int j) { return this.c(i, j); } // Tuinity - OBFHELPER + @Nullable IBlockAccess c(int i, int j); + default void markLightSectionDirty(EnumSkyBlock enumskyblock, SectionPosition sectionposition) { this.a(enumskyblock, sectionposition); } // Tuinity - OBFHELPER default void a(EnumSkyBlock enumskyblock, SectionPosition sectionposition) {} IBlockAccess getWorld(); diff --git a/src/main/java/net/minecraft/server/LightEngineGraphSection.java b/src/main/java/net/minecraft/server/LightEngineGraphSection.java index 13d067f48647dea63ef1bf3a2a3e0868074ba75f..04afd7f285db2f281a038e0be6f557b8a692936b 100644 --- a/src/main/java/net/minecraft/server/LightEngineGraphSection.java +++ b/src/main/java/net/minecraft/server/LightEngineGraphSection.java @@ -74,8 +74,10 @@ public abstract class LightEngineGraphSection extends LightEngineGraph { return i == Long.MAX_VALUE ? this.b(j) : k + 1; } + public final int getSource(long coordinate) { return this.b(coordinate); } // Tuinity - OBFHELPER protected abstract int b(long i); + public final void update(long coordinate, int level, boolean flag) { this.b(coordinate, level, flag); } // Tuinity - OBFHELPER public void b(long i, int j, boolean flag) { this.a(Long.MAX_VALUE, i, j, flag); } diff --git a/src/main/java/net/minecraft/server/LightEngineStorage.java b/src/main/java/net/minecraft/server/LightEngineStorage.java index b98e60772bad7e06845b50fdc11e98c0ea775d3d..e0bbfe1422cbad811ecb43d7436380d86b0f8abc 100644 --- a/src/main/java/net/minecraft/server/LightEngineStorage.java +++ b/src/main/java/net/minecraft/server/LightEngineStorage.java @@ -23,7 +23,8 @@ public abstract class LightEngineStorage> e protected final M f; protected final M updating; // Paper - diff on change, should be "updating" protected final LongSet g = new LongOpenHashSet(); protected final LongSet h = new LongOpenHashSet(); LongSet dirty = h; // Paper - OBFHELPER - protected final Long2ObjectMap i = Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap()); + protected final Long2ObjectOpenHashMap i_synchronized_map_real = new Long2ObjectOpenHashMap<>(); // Tuinity - store wrapped map, we need fastIterator + protected final Long2ObjectMap i = Long2ObjectMaps.synchronize(this.i_synchronized_map_real); // Tuinity - store wrapped map, we need fastIterator private final LongSet n = new LongOpenHashSet(); private final LongSet o = new LongOpenHashSet(); private final LongSet p = new LongOpenHashSet(); @@ -247,7 +248,7 @@ public abstract class LightEngineStorage> e this.p.clear(); this.j = false; - ObjectIterator objectiterator = this.i.long2ObjectEntrySet().iterator(); + ObjectIterator objectiterator = this.i_synchronized_map_real.long2ObjectEntrySet().fastIterator(); // Tuinity - use fast iterator to reduce entry creation Entry entry; long j; @@ -284,7 +285,7 @@ public abstract class LightEngineStorage> e } this.n.clear(); - objectiterator = this.i.long2ObjectEntrySet().iterator(); + objectiterator = this.i_synchronized_map_real.long2ObjectEntrySet().fastIterator(); // Tuinity - use fast iterator to reduce entry creation; while (objectiterator.hasNext()) { entry = (Entry) objectiterator.next(); diff --git a/src/main/java/net/minecraft/server/LightEngineThreaded.java b/src/main/java/net/minecraft/server/LightEngineThreaded.java index 2f9c97dd4e1d705a87772d18c7ab4883a876af08..168fe23177dfaa401396c1e460f56273ee0a59e4 100644 --- a/src/main/java/net/minecraft/server/LightEngineThreaded.java +++ b/src/main/java/net/minecraft/server/LightEngineThreaded.java @@ -2,6 +2,11 @@ package net.minecraft.server; import com.mojang.datafixers.util.Pair; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; // Paper +// Tuinity start +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongIterator; +// Tuinity end import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectList; import it.unimi.dsi.fastutil.objects.ObjectListIterator; @@ -15,11 +20,12 @@ import org.apache.logging.log4j.Logger; public class LightEngineThreaded extends LightEngine implements AutoCloseable { private static final Logger LOGGER = LogManager.getLogger(); - private final ThreadedMailbox b; + private final ThreadedMailbox b; private ThreadedMailbox getExecutor() { return this.b; } // Tuinity - OBFHELPER // Paper start private static final int MAX_PRIORITIES = PlayerChunkMap.GOLDEN_TICKET + 2; private boolean isChunkLightStatus(long pair) { + if (true) return true; // Tuinity - viewing ticket levels async can result in the viewing of transient levels, and LIGHT ticket isn't guaranteed to exist for all loading chunks thanks to really dumb unloading behaviors with the chunk system PlayerChunk playerChunk = playerChunkMap.getVisibleChunk(pair); if (playerChunk == null) { return false; @@ -156,13 +162,218 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { private volatile int f = 5; private final AtomicBoolean g = new AtomicBoolean(); + // Tuinity start - replace light engine impl + protected final com.tuinity.tuinity.chunk.light.StarLightInterface theLightEngine; + public final boolean hasBlockLight; + public final boolean hasSkyLight; + // Tuinity end - replace light engine impl + public LightEngineThreaded(ILightAccess ilightaccess, PlayerChunkMap playerchunkmap, boolean flag, ThreadedMailbox threadedmailbox, Mailbox> mailbox) { super(ilightaccess, true, flag); this.d = playerchunkmap; this.playerChunkMap = d; // Paper this.e = mailbox; this.b = threadedmailbox; + // Tuinity start - replace light engine impl + this.hasBlockLight = true; + this.hasSkyLight = flag; + this.theLightEngine = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? new com.tuinity.tuinity.chunk.light.StarLightInterface(ilightaccess, flag, true) : null; + // Tuinity end - replace light engine impl + } + + // Tuinity start - replace light engine impl + protected final IChunkAccess getChunk(final int chunkX, final int chunkZ) { + final WorldServer world = (WorldServer)this.theLightEngine.getWorld(); + return world.getChunkProvider().getChunkAtImmediately(chunkX, chunkZ); + } + + protected long relightCounter; + + public int relight(java.util.Set chunks_param, + java.util.function.Consumer chunkLightCallback, + java.util.function.IntConsumer onComplete) { + if (!org.bukkit.Bukkit.isPrimaryThread()) { + throw new IllegalStateException("Must only be called on the main thread"); + } + + java.util.Set chunks = new java.util.LinkedHashSet<>(chunks_param); + // add tickets + java.util.Map ticketIds = new java.util.HashMap<>(); + int totalChunks = 0; + for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext();) { + final ChunkCoordIntPair chunkPos = iterator.next(); + + final IChunkAccess chunk = this.theLightEngine.getWorld().getChunkProvider().getChunkAtImmediately(chunkPos.x, chunkPos.z); + if (chunk == null || !chunk.isLit() || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; + } + + final Long id = Long.valueOf(this.relightCounter++); + + this.theLightEngine.getWorld().getChunkProvider().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id); + ticketIds.put(chunkPos, id); + + ++totalChunks; + } + + this.getExecutor().queue(() -> { + this.theLightEngine.relightChunks(chunks, (ChunkCoordIntPair chunkPos) -> { + chunkLightCallback.accept(chunkPos); + ((java.util.concurrent.Executor)this.theLightEngine.getWorld().getChunkProvider().serverThreadQueue).execute(() -> { + this.theLightEngine.getWorld().getChunkProvider().playerChunkMap.getUpdatingChunk(chunkPos.pair()).sendPacketToTrackedPlayers(new PacketPlayOutLightUpdate(chunkPos, this, true), false); + this.theLightEngine.getWorld().getChunkProvider().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos)); + }); + }, onComplete); + }); + this.queueUpdate(); + + return totalChunks; + } + + protected final Long2IntOpenHashMap holdingChunks = new Long2IntOpenHashMap(); + protected final LongArrayList postWorkTicketRelease = new LongArrayList(); + + private void addLightWorkTicket(int chunkX, int chunkZ) { + final long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ); + final int current = this.holdingChunks.putIfAbsent(coordinate, 1); + if (current == 0) { + final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate); + this.theLightEngine.getWorld().getChunkProvider().addTicketAtLevel(TicketType.LIGHT_UPDATE, chunkPos, + MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), chunkPos); + this.theLightEngine.getWorld().getChunkProvider().tickDistanceManager(); + } else { + this.holdingChunks.put(coordinate, current + 1); + } + } + + protected final void releaseLightWorkChunk(int chunkX, int chunkZ) { + final long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ); + final int current = this.holdingChunks.get(coordinate); + if (current == 1) { + this.holdingChunks.remove(coordinate); + final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate); + this.theLightEngine.getWorld().getChunkProvider().removeTicketAtLevel(TicketType.LIGHT_UPDATE, chunkPos, + MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), chunkPos); + } else { + this.holdingChunks.put(coordinate, current - 1); + } + } + + protected final CompletableFuture acquireLightWorkChunk(int chunkX, int chunkZ) { + ChunkProviderServer chunkProvider = this.theLightEngine.getWorld().getChunkProvider(); + PlayerChunkMap chunkMap = chunkProvider.playerChunkMap; + int targetLevel = MCUtil.getTicketLevelFor(ChunkStatus.LIGHT); + + this.addLightWorkTicket(chunkX, chunkZ); + + // light doesn't always load one radius neighbours... + // i.e if they get unloaded + boolean neighboursAtFeatures = true; + int targetNeighbourLevel = MCUtil.getTicketLevelFor(ChunkStatus.LIGHT.getPreviousStatus()); + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + PlayerChunk neighbour = chunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(dx + chunkX, dz + chunkZ)); + ChunkStatus status; + if (neighbour == null || neighbour.getTicketLevel() > targetNeighbourLevel || + (status = neighbour.getChunkHolderStatus()) == null || + !status.isAtLeastStatus(ChunkStatus.LIGHT.getPreviousStatus())) { + neighboursAtFeatures = false; + break; + } + } + } + + PlayerChunk playerChunk = chunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(chunkX, chunkZ)); + ChunkStatus holderStatus; + if (!neighboursAtFeatures || playerChunk == null || playerChunk.getTicketLevel() > targetLevel || + (holderStatus = playerChunk.getChunkHolderStatus()) == null || + !holderStatus.isAtLeastStatus(ChunkStatus.LIGHT)) { + CompletableFuture ret = new CompletableFuture<>(); + + int[] loads = new int[1]; + int requiredLoads = 3 * 3; + java.util.function.Consumer onLoad = (chunk) -> { + if (++loads[0] == requiredLoads) { + ret.complete(this.getChunk(chunkX, chunkZ)); + } + }; + + for (int dx = -1; dx <= 1; ++dx) { + for (int dz = -1; dz <= 1; ++dz) { + chunkProvider.getChunkAtAsynchronously(chunkX + dx, chunkZ + dz, + (dx | dz) == 0 ? ChunkStatus.LIGHT : ChunkStatus.LIGHT.getPreviousStatus(), + true, false, onLoad); + } + } + + return ret; + } + + return CompletableFuture.completedFuture(playerChunk.getAvailableChunkNow()); } + // note: task is discarded if the chunk is not at light status or if the chunk is not lit + protected final void scheduleLightWorkTask(int chunkX, int chunkZ, LightEngineThreaded.Update type, Runnable task) { + if (!org.bukkit.Bukkit.isPrimaryThread()) { + this.playerChunkMap.mainInvokingExecutor.execute(() -> { + this.scheduleLightWorkTask(chunkX, chunkZ, type, task); + }); + return; + } + + IChunkAccess current = this.getChunk(chunkX, chunkZ); + + if (current == null || !current.isLit() || !current.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) { + return; + } + + this.acquireLightWorkChunk(chunkX, chunkZ).whenCompleteAsync((chunk, throwable) -> { + if (throwable != null) { + MinecraftServer.LOGGER.fatal("Failed to load light chunk for light work", throwable); + this.releaseLightWorkChunk(chunkX, chunkZ); + } else { + this.scheduleTask(chunkX, chunkZ, type, () -> { + try { + task.run(); + } finally { + this.postWorkTicketRelease.add(MCUtil.getCoordinateKey(chunkX, chunkZ)); + } + }); + } + }, this.playerChunkMap.mainInvokingExecutor); + } + + // override things from superclass + + @Override + public boolean a() { + return this.theLightEngine != null ? false : super.a(); + } + + @Override + public LightEngineLayerEventListener a(EnumSkyBlock var0) { + if (this.theLightEngine == null) { + return super.a(var0); + } + if (var0 == EnumSkyBlock.BLOCK) { + return this.theLightEngine.getBlockReader(); + } else { + return this.theLightEngine.getSkyReader(); + } + } + + @Override + public int b(BlockPosition var0, int var1) { + if (this.theLightEngine == null) { + return super.b(var0, var1); + } + int var2 = this.theLightEngine.getSkyReader().b(var0) - var1; + int var3 = this.theLightEngine.getBlockReader().b(var0); + return Math.max(var3, var2); + } + // Tuinity end - replace light engine impl + public void close() {} @Override @@ -179,6 +390,15 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { public void a(BlockPosition blockposition) { BlockPosition blockposition1 = blockposition.immutableCopy(); + // Tuinity start - replace light engine impl + if (this.theLightEngine != null) { + this.scheduleLightWorkTask(blockposition1.getX() >> 4, blockposition1.getZ() >> 4, LightEngineThreaded.Update.POST_UPDATE, () -> { + this.theLightEngine.blockChange(blockposition1); + }); + return; + } + // Tuinity start - replace light engine impl + this.a(blockposition.getX() >> 4, blockposition.getZ() >> 4, LightEngineThreaded.Update.POST_UPDATE, SystemUtils.a(() -> { super.a(blockposition1); }, () -> { @@ -187,6 +407,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { } protected void a(ChunkCoordIntPair chunkcoordintpair) { + // Tuinity start - replace light impl + if (this.theLightEngine != null) { + return; + } + // Tuinity end - replace light impl this.a(chunkcoordintpair.x, chunkcoordintpair.z, () -> { return 0; }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> { @@ -211,6 +436,14 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { @Override public void a(SectionPosition sectionposition, boolean flag) { + // Tuinity start - replace light engine impl + if (this.theLightEngine != null) { + this.scheduleLightWorkTask(sectionposition.getX(), sectionposition.getZ(), LightEngineThreaded.Update.POST_UPDATE, () -> { + this.theLightEngine.sectionChange(sectionposition, flag); + }); + return; + } + // Tuinity start - replace light engine impl this.a(sectionposition.a(), sectionposition.c(), () -> { return 0; }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> { @@ -222,6 +455,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { @Override public void a(ChunkCoordIntPair chunkcoordintpair, boolean flag) { + // Tuinity start - replace light impl + if (this.theLightEngine != null) { + return; + } + // Tuinity end - replace light impl this.a(chunkcoordintpair.x, chunkcoordintpair.z, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> { super.a(chunkcoordintpair, flag); }, () -> { @@ -231,6 +469,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { @Override public void a(EnumSkyBlock enumskyblock, SectionPosition sectionposition, @Nullable NibbleArray nibblearray, boolean flag) { + // Tuinity start - replace light impl + if (this.theLightEngine != null) { + return; + } + // Tuinity end - replace light impl this.a(sectionposition.a(), sectionposition.c(), () -> { return 0; }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> { @@ -240,6 +483,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { })); } + private void scheduleTask(int x, int z, LightEngineThreaded.Update lightenginethreaded_update, Runnable runnable) { this.a(x, z, lightenginethreaded_update, runnable); } // Tuinity - OBFHELPER private void a(int i, int j, LightEngineThreaded.Update lightenginethreaded_update, Runnable runnable) { this.a(i, j, this.d.c(ChunkCoordIntPair.pair(i, j)), lightenginethreaded_update, runnable); } @@ -252,6 +496,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { @Override public void b(ChunkCoordIntPair chunkcoordintpair, boolean flag) { + // Tuinity start - replace light impl + if (this.theLightEngine != null) { + return; + } + // Tuinity end - replace light impl this.a(chunkcoordintpair.x, chunkcoordintpair.z, () -> { return 0; }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> { @@ -277,6 +526,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { return; } // Paper end + if (this.theLightEngine == null) { // Tuinity - replace light engine impl ChunkSection[] achunksection = ichunkaccess.getSections(); for (int i = 0; i < 16; ++i) { @@ -293,16 +543,29 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { super.a(blockposition, ichunkaccess.g(blockposition)); }); } + } else { // Tuinity start - replace light engine impl + Boolean[] emptySections = com.tuinity.tuinity.chunk.light.StarLightEngine.getEmptySectionsForChunk(ichunkaccess); + if (!flag) { + this.theLightEngine.lightChunk(ichunkaccess, emptySections); + } else { + this.theLightEngine.forceLoadInChunk(ichunkaccess, emptySections); + // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have + // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should + // catch what we miss here. + this.theLightEngine.checkChunkEdges(chunkcoordintpair.x, chunkcoordintpair.z); + } + + } // Tuinity end - replace light engine impl // this.d.c(chunkcoordintpair); // Paper - move into post task below }, () -> { return "lightChunk " + chunkcoordintpair + " " + flag; // Paper start - merge the 2 together }), () -> { - this.d.c(chunkcoordintpair); // Paper - release light tickets as post task to ensure they stay loaded until fully done + this.d.c(chunkcoordintpair); // Paper - release light tickets as post task to ensure they stay loaded until fully done // Tuinity - diff on change, copied to top of method for early return if the chunk is already lit if (skippedPre[0]) return; // Paper - future's already complete ichunkaccess.b(true); - super.b(chunkcoordintpair, false); + if (this.theLightEngine == null) super.b(chunkcoordintpair, false); // Tuinity - replace light engine impl // Paper start future.complete(ichunkaccess); }); @@ -311,7 +574,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { } public void queueUpdate() { - if ((!this.queue.isEmpty() || super.a()) && this.g.compareAndSet(false, true)) { // Paper + if ((!this.queue.isEmpty() || (this.theLightEngine == null && super.a())) && this.g.compareAndSet(false, true)) { // Paper // Tuinity - replace light impl this.b.a((() -> { // Paper - decompile error this.b(); this.g.set(false); @@ -325,17 +588,36 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable { private final java.util.List pre = new java.util.ArrayList<>(); private final java.util.List post = new java.util.ArrayList<>(); private void b() { + //final long start = System.nanoTime(); // TODO remove debug if (queue.poll(pre, post)) { pre.forEach(Runnable::run); pre.clear(); - super.a(Integer.MAX_VALUE, true, true); + if (this.theLightEngine == null) super.a(Integer.MAX_VALUE, true, true); // Tuinity - replace light impl post.forEach(Runnable::run); post.clear(); } else { // might have level updates to go still - super.a(Integer.MAX_VALUE, true, true); + if (this.theLightEngine == null) super.a(Integer.MAX_VALUE, true, true); // Tuinity - replace light impl + } + // Tuinity start - replace light impl + if (this.theLightEngine != null) { + this.theLightEngine.propagateChanges(); + if (!this.postWorkTicketRelease.isEmpty()) { + LongArrayList copy = this.postWorkTicketRelease.clone(); + this.postWorkTicketRelease.clear(); + this.playerChunkMap.mainInvokingExecutor.execute(() -> { + LongIterator iterator = copy.iterator(); + while (iterator.hasNext()) { + long coordinate = iterator.nextLong(); + this.releaseLightWorkChunk(MCUtil.getCoordinateX(coordinate), MCUtil.getCoordinateZ(coordinate)); + } + }); + } } + // Tuinity end - replace light impl // Paper end + //final long end = System.nanoTime(); // TODO remove debug + //System.out.println("Block updates took " + (end - start) * 1.0e-6 + "ms"); // TODO remove debug } public void a(int i) { diff --git a/src/main/java/net/minecraft/server/LoginListener.java b/src/main/java/net/minecraft/server/LoginListener.java index c61cd50df0c81f7ab12bd0c955fd6f07f2b02e64..d987483255195c0bde713a92676baced1eaff2b3 100644 --- a/src/main/java/net/minecraft/server/LoginListener.java +++ b/src/main/java/net/minecraft/server/LoginListener.java @@ -234,7 +234,7 @@ public class LoginListener implements PacketLoginInListener { s = (new BigInteger(MinecraftEncryption.a("", this.server.getKeyPair().getPublic(), this.loginKey))).toString(16); this.g = LoginListener.EnumProtocolState.AUTHENTICATING; - this.networkManager.a(cipher, cipher1); + this.networkManager.a(this.loginKey); // Tuinity } catch (CryptographyException cryptographyexception) { throw new IllegalStateException("Protocol error", cryptographyexception); } diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java index ff74be14512a947e81b62d53e616131ca7d7f609..e79e773f2219f9a9ae076fcbc8108b792201b11a 100644 --- a/src/main/java/net/minecraft/server/MCUtil.java +++ b/src/main/java/net/minecraft/server/MCUtil.java @@ -38,6 +38,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; public final class MCUtil { + public static final double COLLISION_EPSILON = 1.0E-7; // Tuinity - Just in case mojang changes this... public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor( 0, 2, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(), @@ -221,6 +222,63 @@ public final class MCUtil { return getBlockKey(getBlockCoordinate(entity.locX()), getBlockCoordinate(entity.locY()), getBlockCoordinate(entity.locZ())); } + // Tuinity start + + static final int SECTION_X_BITS = 22; + static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; + static final int SECTION_Y_BITS = 20; + static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; + static final int SECTION_Z_BITS = 22; + static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; + // format is y,z,x (in order of LSB to MSB) + static final int SECTION_Y_SHIFT = 0; + static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; + static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; + static final int SECTION_TO_BLOCK_SHIFT = 4; + + public static long getSectionKey(final int x, final int y, final int z) { + return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getSectionKey(final SectionPosition pos) { + return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getSectionKey(final ChunkCoordIntPair pos, final int y) { + return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static long getSectionKey(final BlockPosition pos) { + return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | + ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | + (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); + } + + public static long getSectionKey(final Entity entity) { + return ((MCUtil.fastFloor(entity.locX()) & SECTION_X_MASK) << SECTION_X_SHIFT) + | ((MCUtil.fastFloor(entity.locY()) & SECTION_Y_MASK) << SECTION_Y_SHIFT) + | ((MCUtil.fastFloor(entity.locZ()) & SECTION_Z_MASK) << SECTION_Z_SHIFT); + } + + public static int getSectionX(final long key) { + return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); + } + + public static int getSectionY(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); + } + + public static int getSectionZ(final long key) { + return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); + } + // Tuinity end + // assumes the sets have the same comparator, and if this comparator is null then assume T is Comparable public static void mergeSortedSets(final java.util.function.Consumer consumer, final java.util.Comparator comparator, final java.util.SortedSet...sets) { final ObjectRBTreeSet all = new ObjectRBTreeSet<>(comparator); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 8c384171ca56a0989ffef1813ad0a9ee3ea31d29..45e310e249a83714d0001d85b2ead8d4f8a2d742 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -151,6 +151,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; public boolean serverAutoSave = false; // Paper @@ -749,10 +750,11 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant= MAX_CHUNK_EXEC_TIME) { + if (!moreTasks) { + lastMidTickExecuteFailure = currTime; + } + + // note: negative values reduce the time + long overuse = diff - MAX_CHUNK_EXEC_TIME; + if (overuse >= (10L * 1000L * 1000L)) { // 10ms + // make sure something like a GC or dumb plugin doesn't screw us over... + overuse = 10L * 1000L * 1000L; // 10ms + } + + double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; + long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); + + lastMidTickExecute = currTime + extraSleep; + return; + } + } + } finally { + co.aikar.timings.MinecraftTimings.midTickChunkTasks.stopTiming(); + } + } + // Tuinity end - execute chunk tasks mid tick + private void executeModerately() { this.executeAll(); java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L); @@ -1088,22 +1162,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant { - midTickLoadChunks(); // will only do loads since we are still considered !canSleepForTick + // Tuinity - replace logic return !this.canOversleep(); }); isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); @@ -1262,6 +1322,8 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant // Spigot - Spigot > // CraftBukkit - cb > vanilla! + return "Tuinity"; // Tuinity //Paper - Paper > // Spigot - Spigot > // CraftBukkit - cb > vanilla! } public CrashReport b(CrashReport crashreport) { diff --git a/src/main/java/net/minecraft/server/NavigationAbstract.java b/src/main/java/net/minecraft/server/NavigationAbstract.java index 1558c5f8256f50be6850f1d7f70eee3e8ec76496..b92ca4a6de01f3f86367fb8dfe3591b08a3e9218 100644 --- a/src/main/java/net/minecraft/server/NavigationAbstract.java +++ b/src/main/java/net/minecraft/server/NavigationAbstract.java @@ -21,7 +21,7 @@ public abstract class NavigationAbstract { protected long j; protected double k; protected float l; - protected boolean m; + protected boolean m; protected final boolean needsPathRecalculation() { return this.m; } // Tuinity - OBFHELPER protected long n; protected PathfinderAbstract o; private BlockPosition p; @@ -30,6 +30,13 @@ public abstract class NavigationAbstract { private final Pathfinder s; public Pathfinder getPathfinder() { return this.s; } // Paper - OBFHELPER private boolean t; + // Tuinity start + public boolean isViableForPathRecalculationChecking() { + return !this.needsPathRecalculation() && + (this.c != null && !this.c.c() && this.c.e() != 0); + } + // Tuinity end + public NavigationAbstract(EntityInsentient entityinsentient, World world) { this.g = Vec3D.ORIGIN; this.h = BaseBlockPosition.ZERO; @@ -85,7 +92,7 @@ public abstract class NavigationAbstract { @Nullable public PathEntity a(Stream stream, int i) { - return this.a((Set) stream.collect(Collectors.toSet()), 8, false, i); + return this.a((Set) stream.collect(Collectors.toSet()), 8, false, i); // Tuinity - diff on change, inlined into SensorNearestBed } @Nullable @@ -393,7 +400,7 @@ public abstract class NavigationAbstract { } public void b(BlockPosition blockposition) { - if (this.c != null && !this.c.c() && this.c.e() != 0) { + if (this.c != null && !this.c.c() && this.c.e() != 0) { // Tuinity - diff on change - needed for isViableForPathRecalculationChecking() PathPoint pathpoint = this.c.d(); Vec3D vec3d = new Vec3D(((double) pathpoint.a + this.a.locX()) / 2.0D, ((double) pathpoint.b + this.a.locY()) / 2.0D, ((double) pathpoint.c + this.a.locZ()) / 2.0D); diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java index fb1e3c705b8abee13695762cdfd0e9f1bfdb5ad8..6a0ec0105399066dede622b45c9471b32c162cf6 100644 --- a/src/main/java/net/minecraft/server/NetworkManager.java +++ b/src/main/java/net/minecraft/server/NetworkManager.java @@ -27,6 +27,8 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; +import io.netty.util.concurrent.AbstractEventExecutor; // Tuinity + public class NetworkManager extends SimpleChannelInboundHandler> { private static final Logger LOGGER = LogManager.getLogger(); @@ -71,6 +73,39 @@ public class NetworkManager extends SimpleChannelInboundHandler> { EnumProtocol protocol; // Paper end + // Tuinity start - allow controlled flushing + volatile boolean canFlush = true; + private final java.util.concurrent.atomic.AtomicInteger packetWrites = new java.util.concurrent.atomic.AtomicInteger(); + private int flushPacketsStart; + private final Object flushLock = new Object(); + + void disableAutomaticFlush() { + synchronized (this.flushLock) { + this.flushPacketsStart = this.packetWrites.get(); // must be volatile and before canFlush = false + this.canFlush = false; + } + } + + void enableAutomaticFlush() { + synchronized (this.flushLock) { + this.canFlush = true; + if (this.packetWrites.get() != this.flushPacketsStart) { // must be after canFlush = true + this.flush(); // only make the flush call if we need to + } + } + } + + private final void flush() { + if (this.channel.eventLoop().inEventLoop()) { + this.channel.flush(); + } else { + this.channel.eventLoop().execute(() -> { + this.channel.flush(); + }); + } + } + // Tuinity end - allow controlled flushing + public NetworkManager(EnumProtocolDirection enumprotocoldirection) { this.h = enumprotocoldirection; } @@ -145,8 +180,63 @@ public class NetworkManager extends SimpleChannelInboundHandler> { if (MinecraftServer.getServer().isDebugging()) throwable.printStackTrace(); // Spigot } + // Tuinity start - packet limiter + protected final Object PACKET_LIMIT_LOCK = new Object(); + protected final com.tuinity.tuinity.util.IntervalledCounter allPacketCounts = com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit != null ? new com.tuinity.tuinity.util.IntervalledCounter( + (long)(com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.packetLimitInterval * 1.0e9) + ) : null; + protected final java.util.Map>, com.tuinity.tuinity.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>(); + + private boolean stopReadingPackets; + private void killForPacketSpam() { + this.sendPacket(new PacketPlayOutKickDisconnect(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]), (future) -> { + this.close(org.bukkit.craftbukkit.util.CraftChatMessage.fromString(com.tuinity.tuinity.config.TuinityConfig.kickMessage, true)[0]); + }); + this.stopReading(); + this.stopReadingPackets = true; + } + // Tuinity end - packet limiter protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet packet) throws Exception { if (this.channel.isOpen()) { + // Tuinity start - packet limiter + if (this.stopReadingPackets) { + return; + } + if (this.allPacketCounts != null || + com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.containsKey(packet.getClass())) { + long time = System.nanoTime(); + synchronized (PACKET_LIMIT_LOCK) { + if (this.allPacketCounts != null) { + this.allPacketCounts.updateAndAdd(1, time); + if (this.allPacketCounts.getRate() >= com.tuinity.tuinity.config.TuinityConfig.allPacketsLimit.maxPacketRate) { + this.killForPacketSpam(); + return; + } + } + + for (Class check = packet.getClass(); check != Object.class; check = check.getSuperclass()) { + com.tuinity.tuinity.config.TuinityConfig.PacketLimit packetSpecificLimit = + com.tuinity.tuinity.config.TuinityConfig.packetSpecificLimits.get(check); + if (packetSpecificLimit == null) { + continue; + } + com.tuinity.tuinity.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> { + return new com.tuinity.tuinity.util.IntervalledCounter((long)(packetSpecificLimit.packetLimitInterval * 1.0e9)); + }); + counter.updateAndAdd(1, time); + if (counter.getRate() >= packetSpecificLimit.maxPacketRate) { + switch (packetSpecificLimit.violateAction) { + case DROP: + return; + case KICK: + this.killForPacketSpam(); + return; + } + } + } + } + } + // Tuinity end - packet limiter try { a(packet, this.packetListener); } catch (CancelledPacketHandleException cancelledpackethandleexception) { @@ -222,7 +312,7 @@ public class NetworkManager extends SimpleChannelInboundHandler> { MCUtil.isMainThread() && packet.isReady() && this.packetQueue.isEmpty() && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty()) ))) { - this.dispatchPacket(packet, genericfuturelistener); + this.writePacket(packet, genericfuturelistener, null); // Tuinity return; } // write the packets to the queue, then flush - antixray hooks there already @@ -248,6 +338,14 @@ public class NetworkManager extends SimpleChannelInboundHandler> { private void dispatchPacket(Packet packet, @Nullable GenericFutureListener> genericFutureListener) { this.b(packet, genericFutureListener); } // Paper - OBFHELPER private void b(Packet packet, @Nullable GenericFutureListener> genericfuturelistener) { + // Tuinity start - add flush parameter + this.writePacket(packet, genericfuturelistener, Boolean.TRUE); + } + private void writePacket(Packet packet, @Nullable GenericFutureListener> genericfuturelistener, Boolean flushConditional) { + this.packetWrites.getAndIncrement(); // must be befeore using canFlush + boolean effectiveFlush = flushConditional == null ? this.canFlush : flushConditional.booleanValue(); + final boolean flush = effectiveFlush || packet instanceof PacketPlayOutKeepAlive || packet instanceof PacketPlayOutKickDisconnect; // no delay for certain packets + // Tuinity end - add flush parameter EnumProtocol enumprotocol = EnumProtocol.a(packet); EnumProtocol enumprotocol1 = (EnumProtocol) this.channel.attr(NetworkManager.c).get(); @@ -270,7 +368,7 @@ public class NetworkManager extends SimpleChannelInboundHandler> { try { // Paper end - ChannelFuture channelfuture = this.channel.writeAndFlush(packet); + ChannelFuture channelfuture = (flush) ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter if (genericfuturelistener != null) { channelfuture.addListener(genericfuturelistener); @@ -290,39 +388,83 @@ public class NetworkManager extends SimpleChannelInboundHandler> { } // Paper end } else { - this.channel.eventLoop().execute(() -> { - if (enumprotocol != enumprotocol1) { - this.setProtocol(enumprotocol); - } + // Tuinity start - optimise packets that are not flushed + Runnable choice1 = null; + AbstractEventExecutor.LazyRunnable choice2 = null; + // note: since the type is not dynamic here, we need to actually copy the old executor code + // into two branches. On conflict, just re-copy - no changes were made inside the executor code. + if (flush) { + choice1 = () -> { + if (enumprotocol != enumprotocol1) { + this.setProtocol(enumprotocol); + } - // Paper start - if (!isConnected()) { - packet.onPacketDispatchFinish(player, null); - return; - } - try { + // Paper start + if (!isConnected()) { + packet.onPacketDispatchFinish(player, null); + return; + } + try { + // Paper end + ChannelFuture channelfuture1 = (flush) ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter + + + if (genericfuturelistener != null) { + channelfuture1.addListener(genericfuturelistener); + } + // Paper start + if (packet.hasFinishListener()) { + channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture)); + } + // Paper end + + channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + // Paper start + } catch (Exception e) { + LOGGER.error("NetworkException: " + player, e); + close(new ChatMessage("disconnect.genericReason", "Internal Exception: " + e.getMessage()));; + packet.onPacketDispatchFinish(player, null); + } // Paper end - ChannelFuture channelfuture1 = this.channel.writeAndFlush(packet); - - - if (genericfuturelistener != null) { - channelfuture1.addListener(genericfuturelistener); - } - // Paper start - if (packet.hasFinishListener()) { - channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture)); - } - // Paper end + }; + } else { + // explicitly declare a variable to make the lambda use the type + choice2 = () -> { + if (enumprotocol != enumprotocol1) { + this.setProtocol(enumprotocol); + } - channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); - // Paper start - } catch (Exception e) { - LOGGER.error("NetworkException: " + player, e); - close(new ChatMessage("disconnect.genericReason", "Internal Exception: " + e.getMessage()));; - packet.onPacketDispatchFinish(player, null); - } - // Paper end - }); + // Paper start + if (!isConnected()) { + packet.onPacketDispatchFinish(player, null); + return; + } + try { + // Paper end + ChannelFuture channelfuture1 = (flush) ? this.channel.writeAndFlush(packet) : this.channel.write(packet); // Tuinity - add flush parameter + + + if (genericfuturelistener != null) { + channelfuture1.addListener(genericfuturelistener); + } + // Paper start + if (packet.hasFinishListener()) { + channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(player, channelFuture)); + } + // Paper end + + channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); + // Paper start + } catch (Exception e) { + LOGGER.error("NetworkException: " + player, e); + close(new ChatMessage("disconnect.genericReason", "Internal Exception: " + e.getMessage()));; + packet.onPacketDispatchFinish(player, null); + } + // Paper end + }; + } + this.channel.eventLoop().execute(choice1 != null ? choice1 : choice2); + // Tuinity end - optimise packets that are not flushed } } @@ -345,6 +487,8 @@ public class NetworkManager extends SimpleChannelInboundHandler> { } private boolean processQueue() { if (this.packetQueue.isEmpty()) return true; + final boolean needsFlush = this.canFlush; // Tuinity - make only one flush call per sendPacketQueue() call + boolean hasWrotePacket = false; // If we are on main, we are safe here in that nothing else should be processing queue off main anymore // But if we are not on main due to login/status, the parent is synchronized on packetQueue java.util.Iterator iterator = this.packetQueue.iterator(); @@ -352,16 +496,22 @@ public class NetworkManager extends SimpleChannelInboundHandler> { NetworkManager.QueuedPacket queued = iterator.next(); // poll -> peek // Fix NPE (Spigot bug caused by handleDisconnection()) - if (queued == null) { + if (false && queued == null) { // Tuinity - diff on change, this logic is redundant: iterator guarantees ret of an element - on change, hook the flush logic here return true; } Packet packet = queued.getPacket(); if (!packet.isReady()) { + // Tuinity start - make only one flush call per sendPacketQueue() call + if (hasWrotePacket && (needsFlush || this.canFlush)) { + this.flush(); + } + // Tuinity end - make only one flush call per sendPacketQueue() call return false; } else { iterator.remove(); - this.dispatchPacket(packet, queued.getGenericFutureListener()); + this.writePacket(packet, queued.getGenericFutureListener(), (!iterator.hasNext() && (needsFlush || this.canFlush)) ? Boolean.TRUE : Boolean.FALSE); // Tuinity - make only one flush call per sendPacketQueue() call + hasWrotePacket = true; // Tuinity - make only one flush call per sendPacketQueue() call } } return true; @@ -438,10 +588,16 @@ public class NetworkManager extends SimpleChannelInboundHandler> { return this.channel instanceof LocalChannel || this.channel instanceof LocalServerChannel; } - public void a(Cipher cipher, Cipher cipher1) { + public void a(javax.crypto.SecretKey secretkey) { // Tuinity this.n = true; - this.channel.pipeline().addBefore("splitter", "decrypt", new PacketDecrypter(cipher)); - this.channel.pipeline().addBefore("prepender", "encrypt", new PacketEncrypter(cipher1)); + // Tuinity start + try { + this.channel.pipeline().addBefore("splitter", "decrypt", new PacketDecrypter(/*MinecraftEncryption.a(2, secretkey)*/ secretkey)); + this.channel.pipeline().addBefore("prepender", "encrypt", new PacketEncrypter(/*MinecraftEncryption.a(1, secretkey)*/ secretkey)); + } catch (java.security.GeneralSecurityException e) { + throw new RuntimeException("Couldn't enable encryption", e); + } + // Tuinity end } public boolean isConnected() { diff --git a/src/main/java/net/minecraft/server/NibbleArray.java b/src/main/java/net/minecraft/server/NibbleArray.java index 4085426af03f032cf405bdfd1e40a8e5dc27c1d1..348d16ddec3b4da0b6b4e4f49916b966005b5259 100644 --- a/src/main/java/net/minecraft/server/NibbleArray.java +++ b/src/main/java/net/minecraft/server/NibbleArray.java @@ -56,6 +56,7 @@ public class NibbleArray { boolean poolSafe = false; public java.lang.Runnable cleaner; private void registerCleaner() { + if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) return; // Tuinity - purge cleaner usage if (!poolSafe) { cleaner = MCUtil.registerCleaner(this, this.a, NibbleArray::releaseBytes); } else { @@ -63,7 +64,7 @@ public class NibbleArray { } } // Paper end - @Nullable protected byte[] a; + @Nullable protected byte[] a; public final byte[] justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist() { return this.a; } public NibbleArray() {} @@ -74,7 +75,7 @@ public class NibbleArray { } public NibbleArray(byte[] abyte, boolean isSafe) { this.a = abyte; - if (!isSafe) this.a = getCloneIfSet(); // Paper - clone for safety + if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && !isSafe) this.a = getCloneIfSet(); // Paper - clone for safety // Tuinity - no need to clone registerCleaner(); // Paper end if (abyte.length != 2048) { @@ -162,7 +163,7 @@ public class NibbleArray { public NibbleArray copy() { return this.b(); } // Paper - OBFHELPER public NibbleArray b() { - return this.a == null ? new NibbleArray() : new NibbleArray(this.a); // Paper - clone in ctor + return this.a == null ? new NibbleArray() : new NibbleArray(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? this.a.clone() : this.a); // Paper - clone in ctor // Tuinity - no longer clone in constructor } public String toString() { diff --git a/src/main/java/net/minecraft/server/PacketCompressor.java b/src/main/java/net/minecraft/server/PacketCompressor.java index 3cdd07cad85ef2d2c4b6c27a55a878695b4a7b12..50b2a8dfbdd0fe60e295d7c7214d7c99bcbeb19a 100644 --- a/src/main/java/net/minecraft/server/PacketCompressor.java +++ b/src/main/java/net/minecraft/server/PacketCompressor.java @@ -7,14 +7,18 @@ import java.util.zip.Deflater; public class PacketCompressor extends MessageToByteEncoder { - private final byte[] a = new byte[8192]; - private final Deflater b; + // Tuinity start - use Velocity natives +// private final byte[] a = new byte[8192]; +// private final Deflater b; private int c; + private final com.velocitypowered.natives.compression.VelocityCompressor compressor; public PacketCompressor(int i) { this.c = i; - this.b = new Deflater(); +// this.b = new Deflater(); + this.compressor = com.velocitypowered.natives.util.Natives.compress.get().create(-1); } + // Tuinity end protected void encode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, ByteBuf bytebuf1) throws Exception { int i = bytebuf.readableBytes(); @@ -24,24 +28,46 @@ public class PacketCompressor extends MessageToByteEncoder { packetdataserializer.d(0); packetdataserializer.writeBytes(bytebuf); } else { - byte[] abyte = new byte[i]; - - bytebuf.readBytes(abyte); - packetdataserializer.d(abyte.length); - this.b.setInput(abyte, 0, i); - this.b.finish(); - - while (!this.b.finished()) { - int j = this.b.deflate(this.a); - - packetdataserializer.writeBytes(this.a, 0, j); + // Tuinity start - delegate to Velocity natives +// byte[] abyte = new byte[i]; +// +// bytebuf.readBytes(abyte); +// packetdataserializer.d(abyte.length); +// this.b.setInput(abyte, 0, i); +// this.b.finish(); +// +// while (!this.b.finished()) { +// int j = this.b.deflate(this.a); +// +// packetdataserializer.writeBytes(this.a, 0, j); +// } +// +// this.b.reset(); + packetdataserializer.d(i); + ByteBuf source = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelhandlercontext.alloc(), + this.compressor, bytebuf); + try { + this.compressor.deflate(source, bytebuf1); + } finally { + source.release(); } - - this.b.reset(); + // Tuinity end } } + // Tuinity start + @Override + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception { + return com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(ctx.alloc(), this.compressor, msg.readableBytes() + 1); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + this.compressor.close(); + } + // Tuinity end + public void a(int i) { this.c = i; } diff --git a/src/main/java/net/minecraft/server/PacketDecompressor.java b/src/main/java/net/minecraft/server/PacketDecompressor.java index 23c850be0155c1ece807d244117a196488f0a13b..4bab19a52b400071e69b06b940ab6432dfe59a1b 100644 --- a/src/main/java/net/minecraft/server/PacketDecompressor.java +++ b/src/main/java/net/minecraft/server/PacketDecompressor.java @@ -10,13 +10,17 @@ import java.util.zip.Inflater; public class PacketDecompressor extends ByteToMessageDecoder { - private final Inflater a; + // Tuinity start - use Velocity natives + //private final Inflater a; + private final com.velocitypowered.natives.compression.VelocityCompressor compressor; private int b; public PacketDecompressor(int i) { this.b = i; - this.a = new Inflater(); + //this.a = new Inflater(); + this.compressor = com.velocitypowered.natives.util.Natives.compress.get().create(-1); } + // Tuinity end protected void decode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, List list) throws Exception { if (bytebuf.readableBytes() != 0) { @@ -34,20 +38,41 @@ public class PacketDecompressor extends ByteToMessageDecoder { throw new DecoderException("Badly compressed packet - size of " + i + " is larger than protocol maximum of " + 2097152); } - byte[] abyte = new byte[packetdataserializer.readableBytes()]; - - packetdataserializer.readBytes(abyte); - this.a.setInput(abyte); - byte[] abyte1 = new byte[i]; - - this.a.inflate(abyte1); - list.add(Unpooled.wrappedBuffer(abyte1)); - this.a.reset(); + // Tuinity start + ByteBuf compatibleIn = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelhandlercontext.alloc(), compressor, bytebuf); + ByteBuf uncompressed = com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer(channelhandlercontext.alloc(), compressor, i); + try { + compressor.inflate(compatibleIn, uncompressed, i); + list.add(uncompressed); + bytebuf.clear(); + } catch (Exception e) { + uncompressed.release(); + throw e; + } finally { + compatibleIn.release(); + } +// byte[] abyte = new byte[packetdataserializer.readableBytes()]; +// +// packetdataserializer.readBytes(abyte); +// this.a.setInput(abyte); +// byte[] abyte1 = new byte[i]; +// +// this.a.inflate(abyte1); +// list.add(Unpooled.wrappedBuffer(abyte1)); +// this.a.reset(); + // Tuinity end } } } + // Tuinity start + @Override + public void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { + this.compressor.close(); + } + // Tuinity end + public void a(int i) { this.b = i; } diff --git a/src/main/java/net/minecraft/server/PacketDecrypter.java b/src/main/java/net/minecraft/server/PacketDecrypter.java index c85f291c5b22a8e85c7556b220cba698701748f2..771cc0f4fa98be294abba65c83442205b6b0ef0b 100644 --- a/src/main/java/net/minecraft/server/PacketDecrypter.java +++ b/src/main/java/net/minecraft/server/PacketDecrypter.java @@ -8,13 +8,24 @@ import javax.crypto.Cipher; public class PacketDecrypter extends MessageToMessageDecoder { - private final PacketEncryptionHandler a; + // Tuinity start + private final com.velocitypowered.natives.encryption.VelocityCipher cipher; + //private final PacketEncryptionHandler a; - public PacketDecrypter(Cipher cipher) { - this.a = new PacketEncryptionHandler(cipher); + public PacketDecrypter(javax.crypto.SecretKey key /* Cipher cipher */) throws java.security.GeneralSecurityException { + //this.a = new PacketEncryptionHandler(cipher); + this.cipher = com.velocitypowered.natives.util.Natives.cipher.get().forDecryption(key); } protected void decode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, List list) throws Exception { - list.add(this.a.a(channelhandlercontext, bytebuf)); + ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(channelhandlercontext.alloc(), cipher, bytebuf).slice(); + try { + cipher.process(compatible); + list.add(compatible); + } catch (Exception e) { + compatible.release(); // compatible will never be used if we throw an exception + throw e; + } } + // Tuinity end } diff --git a/src/main/java/net/minecraft/server/PacketEncrypter.java b/src/main/java/net/minecraft/server/PacketEncrypter.java index e35369476839e9622520af1027d7478aa6d1b037..aba14794cc4cb114fea17bb92816ac29a64b44f8 100644 --- a/src/main/java/net/minecraft/server/PacketEncrypter.java +++ b/src/main/java/net/minecraft/server/PacketEncrypter.java @@ -5,15 +5,38 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; import javax.crypto.Cipher; -public class PacketEncrypter extends MessageToByteEncoder { +// Tuinity start +// We rewrite this class as the Velocity natives support in-place encryption +import io.netty.handler.codec.MessageToMessageEncoder; // An unfortunate import, but this is required to fix a compiler error +public class PacketEncrypter extends MessageToMessageEncoder { - private final PacketEncryptionHandler a; + private final com.velocitypowered.natives.encryption.VelocityCipher cipher; + //private final PacketEncryptionHandler a; - public PacketEncrypter(Cipher cipher) { - this.a = new PacketEncryptionHandler(cipher); + public PacketEncrypter(javax.crypto.SecretKey key /* Cipher cipher */) throws java.security.GeneralSecurityException { + // this.a = new PacketEncryptionHandler(cipher); + this.cipher = com.velocitypowered.natives.util.Natives.cipher.get().forEncryption(key); } - protected void encode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, ByteBuf bytebuf1) throws Exception { - this.a.a(bytebuf, bytebuf1); +// protected void encode(ChannelHandlerContext channelhandlercontext, ByteBuf bytebuf, ByteBuf bytebuf1) throws Exception { +// this.a.a(bytebuf, bytebuf1); +// } + + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, java.util.List out) throws Exception { + ByteBuf compatible = com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible(ctx.alloc(), this.cipher, msg); + try { + this.cipher.process(compatible); + out.add(compatible); + } catch (Exception e) { + compatible.release(); // compatible will never be used if we throw an exception + throw e; + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + cipher.close(); } } +// Tuinity end diff --git a/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java b/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java index a22f0cccecc85b4e4fe4603bcfa213f15c23db69..6cc4a035c8b1312b59685b20039d5e82bb1e1a3e 100644 --- a/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java +++ b/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java @@ -26,12 +26,12 @@ public class PacketPlayOutLightUpdate implements Packet { @Override public void onPacketDispatch(EntityPlayer player) { - remainingSends.incrementAndGet(); + if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) remainingSends.incrementAndGet(); } @Override public void onPacketDispatchFinish(EntityPlayer player, ChannelFuture future) { - if (remainingSends.decrementAndGet() <= 0) { + if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && remainingSends.decrementAndGet() <= 0) { // incase of any race conditions, schedule this delayed MCUtil.scheduleTask(5, () -> { if (remainingSends.get() == 0) { @@ -44,7 +44,7 @@ public class PacketPlayOutLightUpdate implements Packet { @Override public boolean hasFinishListener() { - return true; + return !com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine; // Tuinity - replace light impl } // Paper end @@ -54,8 +54,8 @@ public class PacketPlayOutLightUpdate implements Packet { this.a = chunkcoordintpair.x; this.b = chunkcoordintpair.z; this.i = flag; - this.g = Lists.newArrayList();cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper - this.h = Lists.newArrayList();cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper + this.g = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage + this.h = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage for (int i = 0; i < 18; ++i) { NibbleArray nibblearray = lightengine.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, -1 + i)); @@ -66,7 +66,7 @@ public class PacketPlayOutLightUpdate implements Packet { this.e |= 1 << i; } else { this.c |= 1 << i; - this.g.add(nibblearray.getCloneIfSet()); // Paper + this.g.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again } } @@ -75,7 +75,7 @@ public class PacketPlayOutLightUpdate implements Packet { this.f |= 1 << i; } else { this.d |= 1 << i; - this.h.add(nibblearray1.getCloneIfSet()); // Paper + this.h.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray1.asBytes() : nibblearray1.getCloneIfSet()); // Paper // Tuinity - don't clone again } } } @@ -88,8 +88,8 @@ public class PacketPlayOutLightUpdate implements Packet { this.i = flag; this.c = i; this.d = j; - this.g = Lists.newArrayList();cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper - this.h = Lists.newArrayList();cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper + this.g = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage + this.h = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage for (int k = 0; k < 18; ++k) { NibbleArray nibblearray; @@ -97,7 +97,7 @@ public class PacketPlayOutLightUpdate implements Packet { if ((this.c & 1 << k) != 0) { nibblearray = lightengine.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, -1 + k)); if (nibblearray != null && !nibblearray.c()) { - this.g.add(nibblearray.getCloneIfSet()); // Paper + this.g.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again } else { this.c &= ~(1 << k); if (nibblearray != null) { @@ -109,7 +109,7 @@ public class PacketPlayOutLightUpdate implements Packet { if ((this.d & 1 << k) != 0) { nibblearray = lightengine.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, -1 + k)); if (nibblearray != null && !nibblearray.c()) { - this.h.add(nibblearray.getCloneIfSet()); // Paper + this.h.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again } else { this.d &= ~(1 << k); if (nibblearray != null) { diff --git a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java index b9276928a58d56ca9aac95d262d8555522946bd7..d5a8036b764699a70a69b7dc3d45ea6d10835c44 100644 --- a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java +++ b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java @@ -19,7 +19,7 @@ public class PacketPlayOutMapChunk implements Packet { @Nullable private int[] e; private byte[] f; private byte[] getData() { return this.f; } // Paper - OBFHELPER - private List g; + private List g; private List getTileEntityData() { return this.g; } // Tuinity - OBFHELPER private boolean h; // Paper start - Async-Anti-Xray - Set the ready flag to true @@ -31,7 +31,9 @@ public class PacketPlayOutMapChunk implements Packet { // Paper start private final java.util.List extraPackets = new java.util.ArrayList<>(); - private static final int TE_LIMIT = Integer.getInteger("Paper.excessiveTELimit", 750); + private static final int TE_LIMIT = Integer.getInteger("tuinity.excessive-te-limit", 750); // Tuinity - handle oversized chunk data packets more robustly + private static final int TE_SPLIT_LIMIT = Math.max(4096 + 1, Integer.getInteger("tuinity.te-split-limit", 15_000)); // Tuinity - handle oversized chunk data packets more robustly + private boolean mustSplit; // Tuinity - handle oversized chunk data packets more robustly @Override public java.util.List getExtraPackets() { @@ -40,7 +42,7 @@ public class PacketPlayOutMapChunk implements Packet { // Paper end // Paper start - Anti-Xray - Add chunk packet info @Deprecated public PacketPlayOutMapChunk(Chunk chunk, int i) { this(chunk, i, true); } // Notice for updates: Please make sure this constructor isn't used anywhere - public PacketPlayOutMapChunk(Chunk chunk, int i, boolean modifyBlocks) { + public PacketPlayOutMapChunk(Chunk chunk, int i, boolean modifyBlocks) { final int chunkSectionBitSet = i; // Tuinity - handle oversized chunk data packets more robustly ChunkPacketInfo chunkPacketInfo = modifyBlocks ? chunk.world.chunkPacketBlockController.getChunkPacketInfo(this, chunk, i) : null; // Paper end ChunkCoordIntPair chunkcoordintpair = chunk.getPos(); @@ -49,27 +51,12 @@ public class PacketPlayOutMapChunk implements Packet { this.b = chunkcoordintpair.z; this.h = i == 65535; this.d = new NBTTagCompound(); - Iterator iterator = chunk.f().iterator(); - - Entry entry; - - while (iterator.hasNext()) { - entry = (Entry) iterator.next(); - if (((HeightMap.Type) entry.getKey()).c()) { - this.d.set(((HeightMap.Type) entry.getKey()).b(), new NBTTagLongArray(((HeightMap) entry.getValue()).a())); - } - } - - if (this.h) { - this.e = chunk.getBiomeIndex().a(); - } - - this.f = new byte[this.a(chunk, i)]; - // Paper start - Anti-Xray - Add chunk packet info - if (chunkPacketInfo != null) { - chunkPacketInfo.setData(this.getData()); - } - this.c = this.writeChunk(new PacketDataSerializer(this.j()), chunk, i, chunkPacketInfo); + // Tuinity - move this after the tile entity logic, we need to determine whether we're going to split + // Tuinity - before writing chunk block data + // Tuinity - note: for future maintenance, git will prefer the smallest diff, so if moving the TE code is + // Tuinity - a smaller diff, do that, else move the chunk writing - this makes sure the start/end is correct + Iterator iterator; // Tuinity - move declaration up + Entry entry; // Tuinity - move delcaration up // Paper end this.g = Lists.newArrayList(); iterator = chunk.getTileEntities().entrySet().iterator(); @@ -82,8 +69,16 @@ public class PacketPlayOutMapChunk implements Packet { int j = blockposition.getY() >> 4; if (this.f() || (i & 1 << j) != 0) { + // Tuinity start - improve oversized chunk data packet handling + ++totalTileEntities; + if (totalTileEntities > TE_SPLIT_LIMIT) { + this.mustSplit = true; + this.getTileEntityData().clear(); + this.extraPackets.clear(); + break; + } // Paper start - improve oversized chunk data packet handling - if (++totalTileEntities > TE_LIMIT) { + if (totalTileEntities > TE_LIMIT) { // Tuinity end - improve oversized chunk data packet handling PacketPlayOutTileEntityData updatePacket = tileentity.getUpdatePacket(); if (updatePacket != null) { this.extraPackets.add(updatePacket); @@ -97,7 +92,42 @@ public class PacketPlayOutMapChunk implements Packet { this.g.add(nbttagcompound); } } + // Tuinity start - moved after tile entity gathering + iterator = chunk.f().iterator(); // Declared earlier + + while (iterator.hasNext()) { + entry = (Entry) iterator.next(); + if (((HeightMap.Type) entry.getKey()).c()) { + this.d.set(((HeightMap.Type) entry.getKey()).b(), new NBTTagLongArray(((HeightMap) entry.getValue()).a())); + } + } + + if (this.h) { + this.e = chunk.getBiomeIndex().a(); + } + + this.f = new byte[this.a(chunk, i)]; + // Paper start - Anti-Xray - Add chunk packet info + if (chunkPacketInfo != null) { + chunkPacketInfo.setData(this.getData()); + } + this.c = this.writeChunk(new PacketDataSerializer(this.j()), chunk, i, chunkPacketInfo); + // Tuinity end - moved after tile entity gathering chunk.world.chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks + // Tuinity start - improve oversized chunk data packet handling + if (this.mustSplit) { + int chunkSectionBitSetCopy = chunkSectionBitSet; + for (int a = 0, len = Integer.bitCount(chunkSectionBitSet); a < len; ++a) { + int trailingBit = com.destroystokyo.paper.util.math.IntegerUtil.getTrailingBit(chunkSectionBitSetCopy); + int sectionIndex = Integer.numberOfTrailingZeros(trailingBit); + chunkSectionBitSetCopy ^= trailingBit; // move on to the next + + if (chunk.getSections()[sectionIndex] != null) { + this.extraPackets.add(new PacketPlayOutMapChunk(chunk, trailingBit)); + } + } + } + // Tuinity end - improve oversized chunk data packet handling } // Paper start - Async-Anti-Xray - Getter and Setter for the ready flag @@ -188,7 +218,7 @@ public class PacketPlayOutMapChunk implements Packet { for (int l = achunksection.length; k < l; ++k) { ChunkSection chunksection = achunksection[k]; - if (chunksection != Chunk.a && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) { + if ((!this.mustSplit && chunksection != Chunk.a) && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) { // Tuinity - improve oversized chunk data packet handling j |= 1 << k; chunksection.writeChunkSection(packetdataserializer, chunkPacketInfo); // Paper - Anti-Xray - Add chunk packet info } @@ -205,7 +235,7 @@ public class PacketPlayOutMapChunk implements Packet { for (int l = achunksection.length; k < l; ++k) { ChunkSection chunksection = achunksection[k]; - if (chunksection != Chunk.a && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) { + if ((!this.mustSplit && chunksection != Chunk.a) && (!this.f() || !chunksection.c()) && (i & 1 << k) != 0) { j += chunksection.j(); } } diff --git a/src/main/java/net/minecraft/server/PathType.java b/src/main/java/net/minecraft/server/PathType.java index fb37f5b500c52f915b4536e5ec35552b75056046..52a2d3db7da3596bfdd6fd51147cc93bbe6c7ed0 100644 --- a/src/main/java/net/minecraft/server/PathType.java +++ b/src/main/java/net/minecraft/server/PathType.java @@ -4,6 +4,8 @@ public enum PathType { BLOCKED(-1.0F), OPEN(0.0F), WALKABLE(0.0F), WALKABLE_DOOR(0.0F), TRAPDOOR(0.0F), FENCE(-1.0F), LAVA(-1.0F), WATER(8.0F), WATER_BORDER(8.0F), RAIL(0.0F), UNPASSABLE_RAIL(-1.0F), DANGER_FIRE(8.0F), DAMAGE_FIRE(16.0F), DANGER_CACTUS(8.0F), DAMAGE_CACTUS(-1.0F), DANGER_OTHER(8.0F), DAMAGE_OTHER(-1.0F), DOOR_OPEN(0.0F), DOOR_WOOD_CLOSED(-1.0F), DOOR_IRON_CLOSED(-1.0F), BREACH(4.0F), LEAVES(-1.0F), STICKY_HONEY(8.0F), COCOA(0.0F); + PathType belowOverride; // Tuinity + private final float y; private PathType(float f) { diff --git a/src/main/java/net/minecraft/server/PathfinderGoalMoveThroughVillage.java b/src/main/java/net/minecraft/server/PathfinderGoalMoveThroughVillage.java index 475c0764b97b056f17720f37b1ca3eb1a2375334..9f48d476c05dbeabbfe3c650ce4ad33ec691a56a 100644 --- a/src/main/java/net/minecraft/server/PathfinderGoalMoveThroughVillage.java +++ b/src/main/java/net/minecraft/server/PathfinderGoalMoveThroughVillage.java @@ -50,7 +50,7 @@ public class PathfinderGoalMoveThroughVillage extends PathfinderGoal { if (!worldserver.a_(blockposition1)) { return Double.NEGATIVE_INFINITY; } else { - Optional optional = worldserver.y().c(VillagePlaceType.b, this::a, blockposition1, 10, VillagePlace.Occupancy.IS_OCCUPIED); + Optional optional = Optional.ofNullable(BehaviorFindPosition.findAnyFirstPoi(worldserver.y(), VillagePlaceType.b, this::a, blockposition1, 10, VillagePlace.Occupancy.IS_OCCUPIED)); // Tuinity - remove streams here return !optional.isPresent() ? Double.NEGATIVE_INFINITY : -((BlockPosition) optional.get()).j(blockposition); } @@ -59,7 +59,7 @@ public class PathfinderGoalMoveThroughVillage extends PathfinderGoal { if (vec3d == null) { return false; } else { - Optional optional = worldserver.y().c(VillagePlaceType.b, this::a, new BlockPosition(vec3d), 10, VillagePlace.Occupancy.IS_OCCUPIED); + Optional optional = Optional.ofNullable(BehaviorFindPosition.findAnyFirstPoi(worldserver.y(), VillagePlaceType.b, this::a, new BlockPosition(vec3d), 10, VillagePlace.Occupancy.IS_OCCUPIED)); // Tuinity - remove streams here if (!optional.isPresent()) { return false; diff --git a/src/main/java/net/minecraft/server/PathfinderNormal.java b/src/main/java/net/minecraft/server/PathfinderNormal.java index 74e81e1e4aea6f74b14a84231ddeb7f2fb845ae7..33804e68931e8b4145b896eedeab79bde78779f2 100644 --- a/src/main/java/net/minecraft/server/PathfinderNormal.java +++ b/src/main/java/net/minecraft/server/PathfinderNormal.java @@ -421,6 +421,12 @@ public class PathfinderNormal extends PathfinderAbstract { if (pathtype == PathType.OPEN && j >= 1) { PathType pathtype1 = b(iblockaccess, blockposition_mutableblockposition.d(i, j - 1, k)); + // Tuinity start - reduce pathfinder branches + if (pathtype1.belowOverride != null) { + pathtype = pathtype1.belowOverride; + } else { + PathType original1 = pathtype1; + // Tuinity end - reduce pathfinder branches pathtype = pathtype1 != PathType.WALKABLE && pathtype1 != PathType.OPEN && pathtype1 != PathType.WATER && pathtype1 != PathType.LAVA ? PathType.WALKABLE : PathType.OPEN; if (pathtype1 == PathType.DAMAGE_FIRE) { pathtype = PathType.DAMAGE_FIRE; @@ -437,6 +443,7 @@ public class PathfinderNormal extends PathfinderAbstract { if (pathtype1 == PathType.STICKY_HONEY) { pathtype = PathType.STICKY_HONEY; } + original1.belowOverride = pathtype; } // Tuinity - reduce pathfinder branches } if (pathtype == PathType.WALKABLE) { @@ -462,22 +469,29 @@ public class PathfinderNormal extends PathfinderAbstract { pathtype = PathType.BLOCKED; } else { // Paper end - + // Tuinity start - reduce pathfinder branching + if (iblockdata.neighbourOverridePathType == PathType.OPEN) { + continue; + } else if (iblockdata.neighbourOverridePathType != null) { + return iblockdata.neighbourOverridePathType; + } + // Tuinity end - reduce pathfinder branching if (iblockdata.a(Blocks.CACTUS)) { - return PathType.DANGER_CACTUS; + return iblockdata.neighbourOverridePathType = PathType.DANGER_CACTUS; // Tuinity - reduce pathfinder branching } if (iblockdata.a(Blocks.SWEET_BERRY_BUSH)) { - return PathType.DANGER_OTHER; + return iblockdata.neighbourOverridePathType = PathType.DANGER_OTHER; // Tuinity - reduce pathfinder branching } if (a(iblockdata)) { - return PathType.DANGER_FIRE; + return iblockdata.neighbourOverridePathType = PathType.DANGER_FIRE; // Tuinity - reduce pathfinder branching } if (iblockdata.getFluid().a((Tag) TagsFluid.WATER)) { // Paper - remove another getType call - return PathType.WATER_BORDER; + return iblockdata.neighbourOverridePathType = PathType.WATER_BORDER; // Tuinity - reduce pathfinder branching } + iblockdata.neighbourOverridePathType = PathType.OPEN; // Tuinity - reduce pathfinder branching } // Paper } } @@ -490,6 +504,20 @@ public class PathfinderNormal extends PathfinderAbstract { protected static PathType b(IBlockAccess iblockaccess, BlockPosition blockposition) { IBlockData iblockdata = iblockaccess.getTypeIfLoaded(blockposition); // Paper if (iblockdata == null) return PathType.BLOCKED; // Paper + // Tuinity start - reduce pathfinder branches + if (iblockdata.staticPathType != null) { + return iblockdata.staticPathType; + } + if (iblockdata.getShapeCache() == null) { + // while it might be called static, it might vary on shape! However, only a few blocks have variable shape. + // So we rarely enter here. + return getStaticTypeSlow(iblockaccess, blockposition, iblockdata); + } else { + return iblockdata.staticPathType = getStaticTypeSlow(iblockaccess, blockposition, iblockdata); + } + } + protected static PathType getStaticTypeSlow(IBlockAccess iblockaccess, BlockPosition blockposition, IBlockData iblockdata) { + // Tuinity end - reduce pathfinder branches Block block = iblockdata.getBlock(); Material material = iblockdata.getMaterial(); diff --git a/src/main/java/net/minecraft/server/PathfinderTargetCondition.java b/src/main/java/net/minecraft/server/PathfinderTargetCondition.java index 253377c6238594de1f76cafcbf8223592e4d3f6b..3ebe3d0dc4c2c6aee6ea349006a74cbe5aa8e78f 100644 --- a/src/main/java/net/minecraft/server/PathfinderTargetCondition.java +++ b/src/main/java/net/minecraft/server/PathfinderTargetCondition.java @@ -51,6 +51,7 @@ public class PathfinderTargetCondition { return this; } + public final boolean test(@Nullable EntityLiving entityliving, EntityLiving entityliving1) { return this.a(entityliving, entityliving1); } // Tuinity - OBFHELPER public boolean a(@Nullable EntityLiving entityliving, EntityLiving entityliving1) { if (entityliving == entityliving1) { return false; diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java index 42b12ad5ba68bdf8f76704ddd970715770183de0..ac82f1791ce07e9a23cf97ca34974ab25e26be46 100644 --- a/src/main/java/net/minecraft/server/PlayerChunk.java +++ b/src/main/java/net/minecraft/server/PlayerChunk.java @@ -56,6 +56,12 @@ public class PlayerChunk { long key = net.minecraft.server.MCUtil.getCoordinateKey(this.location); this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key); this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); + // Tuinity start - optimise checkDespawn + Chunk chunk = this.getFullChunkIfCached(); + if (chunk != null) { + chunk.updateGeneralAreaCache(); + } + // Tuinity end - optimise checkDespawn } // Paper end - optimise isOutsideOfRange // Paper start - optimize chunk status progression without jumping through thread pool @@ -362,7 +368,7 @@ public class PlayerChunk { if (!blockposition.isValidLocation()) return; // Paper - SPIGOT-6086 for all invalid locations; avoid acquiring locks Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance - if (chunk != null) { + if (chunk != null && (blockposition.getY() >= 0 && blockposition.getY() <= 255)) { // Tuinity - updates cannot happen in sections that don't exist byte b0 = (byte) SectionPosition.a(blockposition.getY()); if (b0 < 0 || b0 >= this.dirtyBlocks.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 @@ -377,13 +383,14 @@ public class PlayerChunk { public void a(EnumSkyBlock enumskyblock, int i) { Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance + if (this.getAvailableChunkNow() != null) this.getAvailableChunkNow().setNeedsSaving(true); // Tuinity - always mark as needing saving if (chunk != null) { chunk.setNeedsSaving(true); if (enumskyblock == EnumSkyBlock.SKY) { - this.s |= 1 << i - -1; + this.s |= 1 << (i - -1); // Tuinity - fix mojang oopsie } else { - this.r |= 1 << i - -1; + this.r |= 1 << (i - -1); // Tuinity - fix mojang oopsie } } @@ -421,7 +428,7 @@ public class PlayerChunk { this.a(world, blockposition, iblockdata); } else { ChunkSection chunksection = chunk.getSections()[sectionposition.getY()]; - if (chunksection == null) chunksection = new ChunkSection(sectionposition.getY(), chunk, world, true); // Paper - make a new chunk section if none was found + //if (chunksection == null) chunksection = new ChunkSection(sectionposition.getY(), chunk, world, true); // Paper - make a new chunk section if none was found // Tuinity - handled better by spigot PacketPlayOutMultiBlockChange packetplayoutmultiblockchange = new PacketPlayOutMultiBlockChange(sectionposition, shortset, chunksection, this.x); this.a(packetplayoutmultiblockchange, false); @@ -504,6 +511,7 @@ public class PlayerChunk { // Paper end - per player view distance } + public final CompletableFuture> getOrCreateFuture(ChunkStatus chunkstatus, PlayerChunkMap playerchunkmap) { return this.a(chunkstatus, playerchunkmap); } // Tuinity - OBFHELPER public CompletableFuture> a(ChunkStatus chunkstatus, PlayerChunkMap playerchunkmap) { int i = chunkstatus.c(); CompletableFuture> completablefuture = (CompletableFuture) this.statusFutures.get(i); @@ -559,6 +567,7 @@ public class PlayerChunk { } protected void a(PlayerChunkMap playerchunkmap) { + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticket level update"); // Tuinity ChunkStatus chunkstatus = getChunkStatus(this.oldTicketLevel); ChunkStatus chunkstatus1 = getChunkStatus(this.ticketLevel); boolean flag = this.oldTicketLevel <= PlayerChunkMap.GOLDEN_TICKET; @@ -568,7 +577,8 @@ public class PlayerChunk { // CraftBukkit start // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. if (playerchunk_state.isAtLeast(PlayerChunk.State.BORDER) && !playerchunk_state1.isAtLeast(PlayerChunk.State.BORDER)) { - this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main + this.getStatusFutureUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity Chunk chunk = (Chunk)either.left().orElse(null); if (chunk != null) { playerchunkmap.callbackExecutor.execute(() -> { @@ -633,7 +643,8 @@ public class PlayerChunk { if (!flag2 && flag3) { // Paper start - cache ticking ready status int expectCreateCount = ++this.fullChunkCreateCount; - this.fullChunkFuture = playerchunkmap.b(this); ensureMain(this.fullChunkFuture).thenAccept((either) -> { // Paper - ensure main + this.fullChunkFuture = playerchunkmap.b(this); this.fullChunkFuture.thenAccept((either) -> { // Paper - ensure main // Tuinity - always fired on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full chunk future completion"); // Tuinity if (either.left().isPresent() && PlayerChunk.this.fullChunkCreateCount == expectCreateCount) { // note: Here is a very good place to add callbacks to logic waiting on this. Chunk fullChunk = either.left().get(); @@ -664,7 +675,8 @@ public class PlayerChunk { if (!flag4 && flag5) { // Paper start - cache ticking ready status - this.tickingFuture = playerchunkmap.a(this); ensureMain(this.tickingFuture).thenAccept((either) -> { // Paper - ensure main + this.tickingFuture = playerchunkmap.a(this); this.tickingFuture.thenAccept((either) -> { // Paper - ensure main // Tuinity - always completed on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async ticking chunk future completion"); // Tuinity if (either.left().isPresent()) { // note: Here is a very good place to add callbacks to logic waiting on this. Chunk tickingChunk = either.left().get(); @@ -674,6 +686,9 @@ public class PlayerChunk { // Paper start - rewrite ticklistserver PlayerChunk.this.chunkMap.world.onChunkSetTicking(PlayerChunk.this.location.x, PlayerChunk.this.location.z); // Paper end - rewrite ticklistserver + // Tuinity start - ticking chunk set + PlayerChunk.this.chunkMap.world.getChunkProvider().tickingChunks.add(tickingChunk); + // Tuinity end - ticking chunk set } }); @@ -684,6 +699,12 @@ public class PlayerChunk { if (flag4 && !flag5) { this.tickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage this.tickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE; + // Tuinity start - ticking chunk set + Chunk chunkIfCached = this.getFullChunkIfCached(); + if (chunkIfCached != null) { + this.chunkMap.world.getChunkProvider().tickingChunks.remove(chunkIfCached); + } + // Tuinity end - ticking chunk set } boolean flag6 = playerchunk_state.isAtLeast(PlayerChunk.State.ENTITY_TICKING); @@ -695,13 +716,16 @@ public class PlayerChunk { } // Paper start - cache ticking ready status - this.entityTickingFuture = playerchunkmap.b(this.location); ensureMain(this.entityTickingFuture).thenAccept((either) -> { // Paper ensureMain + this.entityTickingFuture = playerchunkmap.b(this.location); this.entityTickingFuture.thenAccept((either) -> { // Paper ensureMain // Tuinity - always completed on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async entity ticking chunk future completion"); // Tuinity if (either.left().isPresent()) { // note: Here is a very good place to add callbacks to logic waiting on this. Chunk entityTickingChunk = either.left().get(); PlayerChunk.this.isEntityTickingReady = true; - + // Tuinity start - entity ticking chunk set + PlayerChunk.this.chunkMap.world.getChunkProvider().entityTickingChunks.add(entityTickingChunk); + // Tuinity end - entity ticking chunk set } @@ -713,6 +737,12 @@ public class PlayerChunk { if (flag6 && !flag7) { this.entityTickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage this.entityTickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE; + // Tuinity start - entity ticking chunk set + Chunk chunkIfCached = this.getFullChunkIfCached(); + if (chunkIfCached != null) { + this.chunkMap.world.getChunkProvider().entityTickingChunks.remove(chunkIfCached); + } + // Tuinity end - entity ticking chunk set } // Paper start - raise IO/load priority if priority changes, use our preferred priority @@ -738,7 +768,8 @@ public class PlayerChunk { // CraftBukkit start // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. if (!playerchunk_state.isAtLeast(PlayerChunk.State.BORDER) && playerchunk_state1.isAtLeast(PlayerChunk.State.BORDER)) { - this.getStatusFutureUncheckedMain(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main + this.getStatusFutureUnchecked(ChunkStatus.FULL).thenAccept((either) -> { // Paper - ensure main // Tuinity - is always on main + com.tuinity.tuinity.util.TickThread.ensureTickThread("Async full status chunk future completion"); // Tuinity Chunk chunk = (Chunk)either.left().orElse(null); if (chunk != null) { playerchunkmap.callbackExecutor.execute(() -> { diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index 49008cdec739b19409fdaf1b0ed806a6c0e93200..16779ffa00caf32752170700e1d88092802fa932 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -121,31 +121,28 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() public final CallbackExecutor callbackExecutor = new CallbackExecutor(); public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable { - - // Paper start - replace impl with recursive safe multi entry queue - // it's possible to schedule multiple tasks currently, so it's vital we change this impl - // If we recurse into the executor again, we will append to another queue, ensuring task order consistency - private java.util.ArrayDeque queued = new java.util.ArrayDeque<>(); + // Tuinity start - revert paper's change + private Runnable queued; @Override public void execute(Runnable runnable) { AsyncCatcher.catchOp("Callback Executor execute"); - if (queued == null) { - queued = new java.util.ArrayDeque<>(); + if (queued != null) { + MinecraftServer.LOGGER.fatal("Failed to schedule runnable", new IllegalStateException("Already queued")); // Paper - make sure this is printed + throw new IllegalStateException("Already queued"); } - queued.add(runnable); + queued = runnable; } + // Tuinity end - revert paper's change @Override public void run() { AsyncCatcher.catchOp("Callback Executor run"); - if (queued == null) { - return; - } - java.util.ArrayDeque queue = queued; + // Tuinity start - revert paper's change + Runnable task = queued; queued = null; - Runnable task; - while ((task = queue.pollFirst()) != null) { + if (task != null) { + // Tuinity end - revert paper's change task.run(); } } @@ -198,8 +195,12 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceTickMap; public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerViewDistanceNoTickMap; // Paper end - no-tick view distance + // Tuinity start - optimise checkDespawn + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; + // Tuinity end - optimise checkDespawn void addPlayerToDistanceMaps(EntityPlayer player) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update distance maps off of the main thread"); // Tuinity int chunkX = MCUtil.getChunkCoordinate(player.locX()); int chunkZ = MCUtil.getChunkCoordinate(player.locZ()); // Note: players need to be explicitly added to distance maps before they can be updated @@ -227,9 +228,13 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.playerViewDistanceBroadcastMap.add(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured player.needsChunkCenterUpdate = false; // Paper end - no-tick view distance + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap.add(player, chunkX, chunkZ, 33); + // Tuinity end - optimise checkDespawn } void removePlayerFromDistanceMaps(EntityPlayer player) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update distance maps off of the main thread"); // Tuinity // Paper start - use distance map to optimise tracker for (int i = 0, len = TRACKING_RANGE_TYPES.length; i < len; ++i) { this.playerEntityTrackerTrackMaps[i].remove(player); @@ -244,9 +249,13 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.playerViewDistanceTickMap.remove(player); this.playerViewDistanceNoTickMap.remove(player); // Paper end - no-tick view distance + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap.remove(player); + // Tuinity end - optimise checkDespawn } void updateMaps(EntityPlayer player) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update distance maps off of the main thread"); // Tuinity int chunkX = MCUtil.getChunkCoordinate(player.locX()); int chunkZ = MCUtil.getChunkCoordinate(player.locZ()); // Note: players need to be explicitly added to distance maps before they can be updated @@ -274,9 +283,35 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.playerViewDistanceBroadcastMap.update(player, chunkX, chunkZ, effectiveNoTickViewDistance + 1); // clients need an extra neighbour to render the full view distance configured player.needsChunkCenterUpdate = false; // Paper end - no-tick view distance + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap.update(player, chunkX, chunkZ, 33); + // Tuinity end - optimise checkDespawn } // Paper end + // Tuinity start + public static enum RegionData implements com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionDataCreator { + // Tuinity start - optimise notify() + PATHING_NAVIGATORS() { + @Override + public Object createData(com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager regionManager) { + return new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(true); + } + } + // Tuinity end - optimise notify() + ; + + @Override + public Object createData(com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section, + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager regionManager) { + throw new AbstractMethodError(); + } + } + + public final com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager dataRegionManager; + // Tuiniy end + private final java.util.concurrent.ExecutorService lightThread; public PlayerChunkMap(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, IAsyncTaskHandler iasynctaskhandler, ILightAccess ilightaccess, ChunkGenerator chunkgenerator, WorldLoadListener worldloadlistener, Supplier supplier, int i, boolean flag) { super(new File(convertable_conversionsession.a(worldserver.getDimensionKey()), "region"), datafixer, flag); @@ -310,9 +345,10 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.worldLoadListener = worldloadlistener; // Paper start - use light thread + String threadName = ((WorldDataServer)this.world.getWorldData()).getName() + " - Light"; // Tuinity - make sure playerchunkmap instance is not retained by the thread factory ThreadedMailbox lightthreaded; ThreadedMailbox threadedmailbox1 = lightthreaded = ThreadedMailbox.a(lightThread = java.util.concurrent.Executors.newSingleThreadExecutor(r -> { Thread thread = new Thread(r); - thread.setName(((WorldDataServer)world.getWorldData()).getName() + " - Light"); + thread.setName(threadName); // Tuinity - make sure playerchunkmap instance is not retained by the thread factory thread.setDaemon(true); thread.setPriority(Thread.NORM_PRIORITY+1); return thread; @@ -444,6 +480,26 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { PlayerChunkMap.this.sendChunk(player, new ChunkCoordIntPair(rangeX, rangeZ), null, true, false); // unloaded, loaded }); // Paper end - no-tick view distance + // Tuinity start + this.dataRegionManager = new com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager<>(this.world, RegionData.class, 2, (1.0 / 3.0), "Data"); + // Tuinity end + // Tuinity start - optimise checkDespawn + this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, + (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + Chunk chunk = PlayerChunkMap.this.world.getChunkProvider().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); + } + }, + (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + Chunk chunk = PlayerChunkMap.this.world.getChunkProvider().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); + } + }); + // Tuinity end - optimise checkDespawn } // Paper start - Chunk Prioritization public void queueHolderUpdate(PlayerChunk playerchunk) { @@ -756,6 +812,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { @Nullable private PlayerChunk a(long i, int j, @Nullable PlayerChunk playerchunk, int k) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Chunk holder update"); // Tuinity + if (this.unloadingPlayerChunk) { MinecraftServer.LOGGER.fatal("Cannot tick distance manager while unloading playerchunks", new Throwable()); throw new IllegalStateException("Cannot tick distance manager while unloading playerchunks"); } // Tuinity if (k > PlayerChunkMap.GOLDEN_TICKET && j > PlayerChunkMap.GOLDEN_TICKET) { return playerchunk; } else { @@ -778,7 +836,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { playerchunk.a(j); } else { playerchunk = new PlayerChunk(new ChunkCoordIntPair(i), j, this.lightEngine, this.p, this); + this.dataRegionManager.addChunk(playerchunk.location.x, playerchunk.location.z); // Tuinity } + this.getVillagePlace().dequeueUnload(playerchunk.location.pair()); // Tuinity - unload POI data this.updatingChunks.put(i, playerchunk); this.updatingChunksModified = true; @@ -904,7 +964,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } - private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more + static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more // Tuinity - private -> package private protected void unloadChunks(BooleanSupplier booleansupplier) { GameProfilerFiller gameprofilerfiller = this.world.getMethodProfiler(); @@ -970,7 +1030,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, chunkPos.x, chunkPos.z, - poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); + poiData, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority if (!chunk.isNeedsSaving()) { return; @@ -1004,7 +1064,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { asyncSaveData = ChunkRegionLoader.getAsyncSaveData(this.world, chunk); } - this.world.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY, + this.world.asyncChunkTaskManager.scheduleChunkSave(chunkPos.x, chunkPos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, // Tuinity - use normal priority asyncSaveData, chunk); chunk.setLastSaved(this.world.getTime()); @@ -1012,6 +1072,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } // Paper end + boolean unloadingPlayerChunk = false; // Tuinity - do not allow ticket level changes while unloading chunks + private void a(long i, PlayerChunk playerchunk) { CompletableFuture completablefuture = playerchunk.getChunkSave(); Consumer consumer = (ichunkaccess) -> { // CraftBukkit - decompile error @@ -1020,7 +1082,15 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { if (completablefuture1 != completablefuture) { this.a(i, playerchunk); } else { - if (this.pendingUnload.remove(i, playerchunk) && ichunkaccess != null) { + // Tuinity start - do not allow ticket level changes while unloading chunks + org.spigotmc.AsyncCatcher.catchOp("playerchunk unload"); + boolean unloadingBefore = this.unloadingPlayerChunk; + this.unloadingPlayerChunk = true; + try { + // Tuinity end - do not allow ticket level changes while unloading chunks + // Tuinity start + boolean removed; + if ((removed = this.pendingUnload.remove(i, playerchunk)) && ichunkaccess != null) { // Tuinity end if (ichunkaccess instanceof Chunk) { ((Chunk) ichunkaccess).setLoaded(false); } @@ -1044,6 +1114,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.lightEngine.queueUpdate(); this.worldLoadListener.a(ichunkaccess.getPos(), (ChunkStatus) null); } + if (removed) this.dataRegionManager.removeChunk(playerchunk.location.x, playerchunk.location.z); // Tuinity + if (removed) this.getVillagePlace().queueUnload(playerchunk.location.pair(), MinecraftServer.currentTickLong + 1); // Tuinity - unload POI data + } finally { this.unloadingPlayerChunk = unloadingBefore; } // Tuinity - do not allow ticket level changes while unloading chunks } }; @@ -1059,6 +1132,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } protected boolean b() { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update visibleChunks off of the main thread"); // Tuinity if (!this.updatingChunksModified) { return false; } else { @@ -1134,6 +1208,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.getVillagePlace().loadInData(chunkcoordintpair, chunkHolder.poiData); chunkHolder.tasks.forEach(Runnable::run); + this.getVillagePlace().dequeueUnload(chunkcoordintpair.pair()); // Tuinity // Paper end if (chunkHolder.protoChunk != null) {try (Timing ignored2 = this.world.timings.chunkLoadLevelTimer.startTimingIfSync()) { // Paper start - timings // Paper - chunk is created async @@ -1246,7 +1321,10 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } // Paper end this.mailboxWorldGen.a(ChunkTaskQueueSorter.a(playerchunk, runnable)); - }); + }).thenComposeAsync((either) -> { // Tuinity start - force competion on the main thread + return CompletableFuture.completedFuture(either); + }, this.mainInvokingExecutor); + // Tuinity end - force competion on the main thread } protected void c(ChunkCoordIntPair chunkcoordintpair) { @@ -1498,6 +1576,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { } public void setViewDistance(int i) { // Paper - public + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update view distance off of the main thread"); // Tuinity int j = MathHelper.clamp(i + 1, 3, 33); // Paper - diff on change, these make the lower view distance limit 2 and the upper 32 if (j != this.viewDistance) { @@ -1511,6 +1590,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { // Paper start - no-tick view distance public final void setNoTickViewDistance(int viewDistance) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Cannot update view distance off of the main thread"); // Tuinity viewDistance = viewDistance == -1 ? -1 : MathHelper.clamp(viewDistance, 2, 32); this.noTickViewDistance = viewDistance; @@ -1626,7 +1706,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { if (Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) { com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave( this.world, chunkcoordintpair.x, chunkcoordintpair.z, null, nbttagcompound, - com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); + com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - writes are async, no need for priority return; } super.write(chunkcoordintpair, nbttagcompound); @@ -1710,6 +1790,11 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); } // Paper end + // Tuinity start + public PlayerChunk getUnloadingPlayerChunk(int chunkX, int chunkZ) { + return this.pendingUnload.get(ChunkCoordIntPair.pair(chunkX, chunkZ)); + } + // Tuinity end // Paper start - async io @@ -2037,22 +2122,25 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { private final void processTrackQueue() { this.world.timings.tracker1.startTiming(); try { - for (EntityTracker tracker : this.trackedEntities.values()) { - // update tracker entry - tracker.updatePlayers(tracker.tracker.getPlayersInTrackRange()); + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = this.world.getChunkProvider().entityTickingChunks.iterator(); + try { + while (iterator.hasNext()) { + Chunk chunk = iterator.next(); + Entity[] entities = chunk.entities.getRawData(); + for (int i = 0, len = chunk.entities.size(); i < len; ++i) { + Entity entity = entities[i]; + EntityTracker tracker = this.trackedEntities.get(entity.getId()); + if (tracker != null) { + tracker.updatePlayers(tracker.tracker.getPlayersInTrackRange()); + tracker.trackerEntry.tick(); + } + } } - } finally { - this.world.timings.tracker1.stopTiming(); - } - - - this.world.timings.tracker2.startTiming(); - try { - for (EntityTracker tracker : this.trackedEntities.values()) { - tracker.trackerEntry.tick(); + } finally { + iterator.finishedIterating(); } } finally { - this.world.timings.tracker2.stopTiming(); + this.world.timings.tracker1.stopTiming(); } } // Paper end - optimised tracker diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java index 87b1ff21957d5d708209257e569785aeaf191181..5c708ed2cd3b10744b0d6d2eb2ef51d0411ce0dc 100644 --- a/src/main/java/net/minecraft/server/PlayerConnection.java +++ b/src/main/java/net/minecraft/server/PlayerConnection.java @@ -419,7 +419,9 @@ public class PlayerConnection implements PacketListenerPlayIn { speed *= 2f; // TODO: Get the speed of the vehicle instead of the player // Paper start - Prevent moving into unloaded chunks - if (player.world.paperConfig.preventMovingIntoUnloadedChunks && worldserver.getChunkIfLoadedImmediately((int) Math.floor(packetplayinvehiclemove.getX()) >> 4, (int) Math.floor(packetplayinvehiclemove.getZ()) >> 4) == null) { + if (player.world.paperConfig.preventMovingIntoUnloadedChunks // Tuinity - improve this check + && (!worldserver.areChunksLoadedForMove(this.player.getBoundingBoxAt(this.player.locX(), this.player.locY(), this.player.locZ()).expand(toX - this.player.locX(), toY - this.player.locY(), toZ - this.player.locZ()))) // Tuinity - improve this check + || !worldserver.areChunksLoadedForMove(entity.getBoundingBoxAt(entity.locX(), entity.locY(), entity.locZ()).expand(toX - entity.locX(), toY - entity.locY(), toZ - entity.locZ()))) { // Tuinity - improve this check this.networkManager.sendPacket(new PacketPlayOutVehicleMove(entity)); return; } @@ -1068,7 +1070,7 @@ public class PlayerConnection implements PacketListenerPlayIn { } if (this.teleportPos != null) { - if (this.e - this.A > 20) { + if (false && this.e - this.A > 20) { // Tuinity - this will greatly screw with clients with > 1000ms RTT this.A = this.e; this.a(this.teleportPos.x, this.teleportPos.y, this.teleportPos.z, this.player.yaw, this.player.pitch); } @@ -1138,7 +1140,7 @@ public class PlayerConnection implements PacketListenerPlayIn { speed = player.abilities.walkSpeed * 10f; } // Paper start - Prevent moving into unloaded chunks - if (player.world.paperConfig.preventMovingIntoUnloadedChunks && (this.player.locX() != toX || this.player.locZ() != toZ) && worldserver.getChunkIfLoadedImmediately((int) Math.floor(toX) >> 4, (int) Math.floor(toZ) >> 4) == null) { // Paper - use getIfLoadedImmediately + if (player.world.paperConfig.preventMovingIntoUnloadedChunks && !((WorldServer)this.player.world).areChunksLoadedForMove(this.player.getBoundingBoxAt(this.player.locX(), this.player.locY(), this.player.locZ()).expand(toX - this.player.locX(), toY - this.player.locY(), toZ - this.player.locZ()))) { // Paper - use getIfLoadedImmediately // Tuinity - improve this check this.internalTeleport(this.player.locX(), this.player.locY(), this.player.locZ(), this.player.yaw, this.player.pitch, Collections.emptySet()); return; } @@ -1194,6 +1196,7 @@ public class PlayerConnection implements PacketListenerPlayIn { } this.player.move(EnumMoveType.PLAYER, new Vec3D(d7, d8, d9)); + boolean didCollide = toX != this.player.locX() || toY != this.player.locY() || toZ != this.player.locZ(); // Tuinity - needed here as the difference in Y can be reset - also note: this is only a guess at whether collisions took place, floating point errors can make this true when it shouldn't be... this.player.setOnGround(packetplayinflying.b()); // CraftBukkit - SPIGOT-5810, SPIGOT-5835: reset by this.player.move // Paper start - prevent position desync if (this.teleportPos != null) { @@ -1218,7 +1221,7 @@ public class PlayerConnection implements PacketListenerPlayIn { } this.player.setLocation(d4, d5, d6, f, f1); - if (!this.player.noclip && !this.player.isSleeping() && (flag1 && worldserver.getCubes(this.player, axisalignedbb) || this.a((IWorldReader) worldserver, axisalignedbb))) { + if (!this.player.noclip && !this.player.isSleeping() && (flag1 && worldserver.getCubes(this.player, axisalignedbb) || (didCollide && this.a((IWorldReader) worldserver, axisalignedbb)))) { // Tuinity - optimise out the extra getCubes-like call most of the time this.a(d0, d1, d2, f, f1); } else { // CraftBukkit start - fire PlayerMoveEvent diff --git a/src/main/java/net/minecraft/server/PlayerConnectionUtils.java b/src/main/java/net/minecraft/server/PlayerConnectionUtils.java index 7ea293f38dedd6066601d94adbe175a31c502e1f..e698dd22607b2b2c4068c5bfb03ac53eb5bac080 100644 --- a/src/main/java/net/minecraft/server/PlayerConnectionUtils.java +++ b/src/main/java/net/minecraft/server/PlayerConnectionUtils.java @@ -13,10 +13,30 @@ public class PlayerConnectionUtils { ensureMainThread(packet, t0, (IAsyncTaskHandler) worldserver.getMinecraftServer()); } + // Tuinity start - detailed watchdog information + private static final java.util.concurrent.ConcurrentLinkedDeque packetProcessing = new java.util.concurrent.ConcurrentLinkedDeque<>(); + private static final java.util.concurrent.atomic.AtomicLong totalMainThreadPacketsProcessed = new java.util.concurrent.atomic.AtomicLong(); + + public static long getTotalProcessedPackets() { + return totalMainThreadPacketsProcessed.get(); + } + + public static java.util.List getCurrentPacketProcessors() { + java.util.List ret = new java.util.ArrayList<>(4); + for (PacketListener listener : packetProcessing) { + ret.add(listener); + } + + return ret; + } + // Tuinity end - detailed watchdog information + public static void ensureMainThread(Packet packet, T t0, IAsyncTaskHandler iasynctaskhandler) throws CancelledPacketHandleException { if (!iasynctaskhandler.isMainThread()) { Timing timing = MinecraftTimings.getPacketTiming(packet); // Paper - timings iasynctaskhandler.execute(() -> { + packetProcessing.push(t0); // Tuinity - detailed watchdog information + try { // Tuinity - detailed watchdog information if (MinecraftServer.getServer().hasStopped() || (t0 instanceof PlayerConnection && ((PlayerConnection) t0).processedDisconnect)) return; // CraftBukkit, MC-142590 if (t0.a().isConnected()) { try (Timing ignored = timing.startTiming()) { // Paper - timings @@ -40,6 +60,12 @@ public class PlayerConnectionUtils { } else { PlayerConnectionUtils.LOGGER.debug("Ignoring packet due to disconnection: " + packet); } + // Tuinity start - detailed watchdog information + } finally { + totalMainThreadPacketsProcessed.getAndIncrement(); + packetProcessing.pop(); + } + // Tuinity end - detailed watchdog information }); throw CancelledPacketHandleException.INSTANCE; diff --git a/src/main/java/net/minecraft/server/PlayerInteractManager.java b/src/main/java/net/minecraft/server/PlayerInteractManager.java index 114e986e5132e5e4bb42d0f08a067429bce53ba6..05656ea8448aa569e8dd480461e2d5f70d01568b 100644 --- a/src/main/java/net/minecraft/server/PlayerInteractManager.java +++ b/src/main/java/net/minecraft/server/PlayerInteractManager.java @@ -21,14 +21,29 @@ public class PlayerInteractManager { private EnumGamemode gamemode; private EnumGamemode e; private boolean f; - private int lastDigTick; + private int lastDigTick; private long lastDigTime; // Tuinity - lag compensate block breaking private BlockPosition h; private int currentTick; - private boolean j; + private boolean j; private final boolean hasDestroyedTooFast() { return this.j; } // Tuinity - OBFHELPER private BlockPosition k; - private int l; + private int l; private final int getHasDestroyedTooFastStartTick() { return this.l; } // Tuinity - OBFHELPER + private long hasDestroyedTooFastStartTime; // Tuinity - lag compensate block breaking private int m; + // Tuinity start - lag compensate block breaking + private int getTimeDiggingLagCompensate() { + int lagCompensated = (int)((System.nanoTime() - this.lastDigTime) / (50L * 1000L * 1000L)); + int tickDiff = this.currentTick - this.lastDigTick; + return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to + } + + private int getTimeDiggingTooFastLagCompensate() { + int lagCompensated = (int)((System.nanoTime() - this.hasDestroyedTooFastStartTime) / (50L * 1000L * 1000L)); + int tickDiff = this.currentTick - this.getHasDestroyedTooFastStartTick(); + return (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking && lagCompensated > (tickDiff + 1)) ? lagCompensated : tickDiff; // add one to ensure we don't lag compensate unless we need to + } + // Tuinity end + public PlayerInteractManager(WorldServer worldserver) { this.gamemode = EnumGamemode.NOT_SET; this.e = EnumGamemode.NOT_SET; @@ -84,7 +99,7 @@ public class PlayerInteractManager { if (iblockdata == null || iblockdata.isAir()) { // Paper this.j = false; } else { - float f = this.a(iblockdata, this.k, this.l); + float f = this.updateBlockBreakAnimation(iblockdata, this.k, this.getTimeDiggingTooFastLagCompensate()); // Tuinity - lag compensate destroying blocks if (f >= 1.0F) { this.j = false; @@ -104,7 +119,7 @@ public class PlayerInteractManager { this.m = -1; this.f = false; } else { - this.a(iblockdata, this.h, this.lastDigTick); + this.updateBlockBreakAnimation(iblockdata, this.h, this.getTimeDiggingLagCompensate()); // Tuinity - lag compensate destroying blocks } } @@ -112,6 +127,12 @@ public class PlayerInteractManager { private float a(IBlockData iblockdata, BlockPosition blockposition, int i) { int j = this.currentTick - i; + // Tuinity start - change i (startTime) to totalTime + return this.updateBlockBreakAnimation(iblockdata, blockposition, j); + } + private float updateBlockBreakAnimation(IBlockData iblockdata, BlockPosition blockposition, int totalTime) { + int j = totalTime; + // Tuinity end float f = iblockdata.getDamage(this.player, this.player.world, blockposition) * (float) (j + 1); int k = (int) (f * 10.0F); @@ -179,7 +200,7 @@ public class PlayerInteractManager { return; } - this.lastDigTick = this.currentTick; + this.lastDigTick = this.currentTick; this.lastDigTime = System.nanoTime(); // Tuinity - lag compensate block breaking float f = 1.0F; iblockdata = this.world.getType(blockposition); @@ -232,12 +253,12 @@ public class PlayerInteractManager { int j = (int) (f * 10.0F); this.world.a(this.player.getId(), blockposition, j); - this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "actual start of destroying")); + if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "actual start of destroying")); // Tuinity - on lagging servers this can cause the client to think it's only just started to destroy a block when it already has/will this.m = j; } } else if (packetplayinblockdig_enumplayerdigtype == PacketPlayInBlockDig.EnumPlayerDigType.STOP_DESTROY_BLOCK) { if (blockposition.equals(this.h)) { - int k = this.currentTick - this.lastDigTick; + int k = this.getTimeDiggingLagCompensate(); // Tuinity - lag compensate block breaking iblockdata = this.world.getType(blockposition); if (!iblockdata.isAir()) { @@ -254,12 +275,18 @@ public class PlayerInteractManager { this.f = false; this.j = true; this.k = blockposition; - this.l = this.lastDigTick; + this.l = this.lastDigTick; this.hasDestroyedTooFastStartTime = this.lastDigTime; // Tuinity - lag compensate block breaking } } } + // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block + if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) { + this.player.playerConnection.sendPacket(new PacketPlayOutBlockChange(this.world, blockposition)); + } else { this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "stopped destroying")); + } + // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block } else if (packetplayinblockdig_enumplayerdigtype == PacketPlayInBlockDig.EnumPlayerDigType.ABORT_DESTROY_BLOCK) { this.f = false; if (!Objects.equals(this.h, blockposition) && !BlockPosition.ZERO.equals(this.h)) { @@ -271,7 +298,7 @@ public class PlayerInteractManager { } this.world.a(this.player.getId(), blockposition, -1); - this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "aborted destroying")); + if (!com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, "aborted destroying")); // Tuinity - this can cause clients on a lagging server to think they stopped destroying a block they're currently destroying } } @@ -281,7 +308,13 @@ public class PlayerInteractManager { public void a(BlockPosition blockposition, PacketPlayInBlockDig.EnumPlayerDigType packetplayinblockdig_enumplayerdigtype, String s) { if (this.breakBlock(blockposition)) { + // Tuinity start - this can cause clients on a lagging server to think they're not currently destroying a block + if (com.tuinity.tuinity.config.TuinityConfig.lagCompensateBlockBreaking) { + this.player.playerConnection.sendPacket(new PacketPlayOutBlockChange(this.world, blockposition)); + } else { this.player.playerConnection.sendPacket(new PacketPlayOutBlockBreak(blockposition, this.world.getType(blockposition), packetplayinblockdig_enumplayerdigtype, true, s)); + } + // Tuinity end - this can cause clients on a lagging server to think they're not currently destroying a block } else { this.player.playerConnection.sendPacket(new PacketPlayOutBlockChange(this.world, blockposition)); // CraftBukkit - SPIGOT-5196 } diff --git a/src/main/java/net/minecraft/server/ProtoChunk.java b/src/main/java/net/minecraft/server/ProtoChunk.java index 5b0cd414ca1949ab53b289f7159f18da07d21f14..71c7fca4ea925fee8790c5a4042ad26584a8ceb3 100644 --- a/src/main/java/net/minecraft/server/ProtoChunk.java +++ b/src/main/java/net/minecraft/server/ProtoChunk.java @@ -48,6 +48,42 @@ public class ProtoChunk implements IChunkAccess { private volatile boolean u; final World world; // Paper - Anti-Xray - Add world // Paper - private -> default + // Tuinity start - rewrite light engine + private volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(); + private volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(); + private volatile boolean[] emptinessMap; + + @Override + public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() { + return this.blockNibbles; + } + + @Override + public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + this.blockNibbles = nibbles; + } + + @Override + public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() { + return this.skyNibbles; + } + + @Override + public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + this.skyNibbles = nibbles; + } + + @Override + public boolean[] getEmptinessMap() { + return this.emptinessMap; + } + + @Override + public void setEmptinessMap(final boolean[] emptinessMap) { + this.emptinessMap = emptinessMap; + } + // Tuinity end - rewrite light engine + // Paper start - Anti-Xray - Add world @Deprecated public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter) { this(chunkcoordintpair, chunkconverter, null); } // Notice for updates: Please make sure this constructor isn't used anywhere public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter, World world) { @@ -173,20 +209,17 @@ public class ProtoChunk implements IChunkAccess { ChunkSection chunksection = this.a(j >> 4); IBlockData iblockdata1 = chunksection.setType(i & 15, j & 15, k & 15, iblockdata); - if (this.g.b(ChunkStatus.FEATURES) && iblockdata != iblockdata1 && (iblockdata.b((IBlockAccess) this, blockposition) != iblockdata1.b((IBlockAccess) this, blockposition) || iblockdata.f() != iblockdata1.f() || iblockdata.e() || iblockdata1.e())) { + if ((com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (this.g.b(ChunkStatus.LIGHT) && this.isLit()) : (this.g.b(ChunkStatus.FEATURES))) && iblockdata != iblockdata1 && (iblockdata.b((IBlockAccess) this, blockposition) != iblockdata1.b((IBlockAccess) this, blockposition) || iblockdata.f() != iblockdata1.f() || iblockdata.e() || iblockdata1.e())) { // Tuinity - move block updates to only happen after lighting occurs LightEngine lightengine = this.e(); lightengine.a(blockposition); } - EnumSet enumset = this.getChunkStatus().h(); + HeightMap.Type[] enumset = this.getChunkStatus().heightMaps; // Tuinity - reduce iterator creation EnumSet enumset1 = null; - Iterator iterator = enumset.iterator(); + // Tuinity - reduce iterator creation - HeightMap.Type heightmap_type; - - while (iterator.hasNext()) { - heightmap_type = (HeightMap.Type) iterator.next(); + for (HeightMap.Type heightmap_type : enumset) { // Tuinity - reduce iterator creation HeightMap heightmap = (HeightMap) this.f.get(heightmap_type); if (heightmap == null) { @@ -202,10 +235,9 @@ public class ProtoChunk implements IChunkAccess { HeightMap.a(this, enumset1); } - iterator = enumset.iterator(); - - while (iterator.hasNext()) { - heightmap_type = (HeightMap.Type) iterator.next(); + // Tuinity start - reduce iterator creation + for (HeightMap.Type heightmap_type : enumset) { + // Tuinity end - reduce iterator creation ((HeightMap) this.f.get(heightmap_type)).a(i & 15, j, k & 15, iblockdata); } diff --git a/src/main/java/net/minecraft/server/ProtoChunkExtension.java b/src/main/java/net/minecraft/server/ProtoChunkExtension.java index 300cbb8b01d94e7eb0cded0c8e118103c416d4b6..0e528df0d9d77f39af1b5c62a29ef31e6ac3b614 100644 --- a/src/main/java/net/minecraft/server/ProtoChunkExtension.java +++ b/src/main/java/net/minecraft/server/ProtoChunkExtension.java @@ -8,7 +8,39 @@ import javax.annotation.Nullable; public class ProtoChunkExtension extends ProtoChunk { - private final Chunk a; + private final Chunk a; public final Chunk getWrappedChunk() { return this.a; } // Tuinity - OBFHELPER + + // Tuinity start - rewrite light engine + @Override + public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() { + return this.getWrappedChunk().getBlockNibbles(); + } + + @Override + public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + this.getWrappedChunk().setBlockNibbles(nibbles); + } + + @Override + public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() { + return this.getWrappedChunk().getSkyNibbles(); + } + + @Override + public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) { + this.getWrappedChunk().setSkyNibbles(nibbles); + } + + @Override + public boolean[] getEmptinessMap() { + return this.getWrappedChunk().getEmptinessMap(); + } + + @Override + public void setEmptinessMap(final boolean[] emptinessMap) { + this.getWrappedChunk().setEmptinessMap(emptinessMap); + } + // Tuinity end - rewrite light engine public ProtoChunkExtension(Chunk chunk) { super(chunk.getPos(), ChunkConverter.a, chunk.world); // Paper - Anti-Xray - Add parameter diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java index 1751fb6934d9242e475c1a44b2a4a1ade6987766..1ffa213a819f9d39488ca3599f77e771de8081a5 100644 --- a/src/main/java/net/minecraft/server/RegionFile.java +++ b/src/main/java/net/minecraft/server/RegionFile.java @@ -5,6 +5,7 @@ import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -29,15 +30,350 @@ public class RegionFile implements AutoCloseable { private static final Logger LOGGER = LogManager.getLogger(); private static final ByteBuffer c = ByteBuffer.allocateDirect(1); private final FileChannel dataFile; - private final java.nio.file.Path e; - private final RegionFileCompression f; + private final java.nio.file.Path e; private final java.nio.file.Path getContainingDataFolder() { return this.e; } // Tuinity - OBFHELPER + private final RegionFileCompression f; private final RegionFileCompression getRegionFileCompression() { return this.f; } // Tuinity - OBFHELPER private final ByteBuffer g; - private final IntBuffer h; - private final IntBuffer i; + private final IntBuffer h; private final IntBuffer getOffsets() { return this.h; } // Tuinity - OBFHELPER + private final IntBuffer i; private final IntBuffer getTimestamps() { return this.i; } // Tuinity - OBFHELPER @VisibleForTesting protected final RegionFileBitSet freeSectors; public final File file; // Paper + // Tuinity start - try to recover from RegionFile header corruption + private static long roundToSectors(long bytes) { + long sectors = bytes >>> 12; // 4096 = 2^12 + long remainingBytes = bytes & 4095; + long sign = -remainingBytes; // sign is 1 if nonzero + return sectors + (sign >>> 63); + } + + private static final NBTTagCompound OVERSIZED_COMPOUND = new NBTTagCompound(); + + private NBTTagCompound attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException { + try { + if (chunkDataLength < 0) { + return null; + } + + long offset = sector * 4096L + 4L; // offset for chunk data + + if ((offset + chunkDataLength) > fileLength) { + return null; + } + + ByteBuffer chunkData = ByteBuffer.allocate(chunkDataLength); + if (chunkDataLength != this.dataFile.read(chunkData, offset)) { + return null; + } + + ((java.nio.Buffer)chunkData).flip(); + + byte compressionType = chunkData.get(); + if (compressionType < 0) { // compressionType & 128 != 0 + // oversized chunk + return OVERSIZED_COMPOUND; + } + + RegionFileCompression compression = RegionFileCompression.getByType(compressionType); + if (compression == null) { + return null; + } + + InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position())); + + return NBTCompressedStreamTools.readNBT((java.io.DataInput)new DataInputStream(new BufferedInputStream(input))); + } catch (Exception ex) { + return null; + } + } + + private int getLength(long sector) throws IOException { + ByteBuffer length = ByteBuffer.allocate(4); + if (4 != this.dataFile.read(length, sector * 4096L)) { + return -1; + } + + return length.getInt(0); + } + + private void backupRegionFile() { + File backup = new File(this.file.getParent(), this.file.getName() + "." + new java.util.Random().nextLong() + ".backup"); + this.backupRegionFile(backup); + } + + private void backupRegionFile(File to) { + try { + this.dataFile.force(true); + MinecraftServer.LOGGER.warn("Backing up regionfile \"" + this.file.getAbsolutePath() + "\" to " + to.getAbsolutePath()); + java.nio.file.Files.copy(this.file.toPath(), to.toPath()); + MinecraftServer.LOGGER.warn("Backed up the regionfile to " + to.getAbsolutePath()); + } catch (IOException ex) { + MinecraftServer.LOGGER.error("Failed to backup to " + to.getAbsolutePath(), ex); + } + } + + // note: only call for CHUNK regionfiles + void recalculateHeader() throws IOException { + if (!this.canRecalcHeader) { + return; + } + synchronized (this) { + MinecraftServer.LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.file.getAbsolutePath(), new Throwable()); + + // try to backup file so maybe it could be sent to us for further investigation + + this.backupRegionFile(); + NBTTagCompound[] compounds = new NBTTagCompound[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data) + int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes + int[] sectorOffsets = new int[32 * 32]; // in sectors + boolean[] hasAikarOversized = new boolean[32 * 32]; + + long fileLength = this.dataFile.size(); + long totalSectors = roundToSectors(fileLength); + + // search the regionfile from start to finish for the most up-to-date chunk data + + for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip + int chunkDataLength = this.getLength(i); + NBTTagCompound compound = this.attemptRead(i, chunkDataLength, fileLength); + if (compound == null || compound == OVERSIZED_COMPOUND) { + continue; + } + + ChunkCoordIntPair chunkPos = ChunkRegionLoader.getChunkCoordinate(compound); + int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); + + NBTTagCompound otherCompound = compounds[location]; + + if (otherCompound != null && ChunkRegionLoader.getLastWorldSaveTime(otherCompound) > ChunkRegionLoader.getLastWorldSaveTime(compound)) { + continue; // don't overwrite newer data. + } + + // aikar oversized? + File aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); + boolean isAikarOversized = false; + if (aikarOversizedFile.exists()) { + try { + NBTTagCompound aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); + if (ChunkRegionLoader.getLastWorldSaveTime(compound) == ChunkRegionLoader.getLastWorldSaveTime(aikarOversizedCompound)) { + // best we got for an id. hope it's good enough + isAikarOversized = true; + } + } catch (Exception ex) { + MinecraftServer.LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.file.getAbsolutePath() + ", oversized data for this chunk will be lost", ex); + // fall through, if we can't read aikar oversized we can't risk corrupting chunk data + } + } + + hasAikarOversized[location] = isAikarOversized; + compounds[location] = compound; + rawLengths[location] = chunkDataLength + 4; + sectorOffsets[location] = (int)i; + + int chunkSectorLength = (int)roundToSectors(rawLengths[location]); + i += chunkSectorLength; + --i; // gets incremented next iteration + } + + // forge style oversized data is already handled by the local search, and aikar data we just hope + // we get it right as aikar data has no identifiers we could use to try and find its corresponding + // local data compound + + java.nio.file.Path containingFolder = this.getContainingDataFolder(); + File[] regionFiles = containingFolder.toFile().listFiles(); + boolean[] oversized = new boolean[32 * 32]; + RegionFileCompression[] oversizedCompressionTypes = new RegionFileCompression[32 * 32]; + + if (regionFiles != null) { + ChunkCoordIntPair ourLowerLeftPosition = RegionFileCache.getRegionFileCoordinates(this.file); + + if (ourLowerLeftPosition == null) { + MinecraftServer.LOGGER.fatal("Unable to get chunk location of regionfile " + this.file.getAbsolutePath() + ", cannot recover oversized chunks"); + } else { + int lowerXBound = ourLowerLeftPosition.x; // inclusive + int lowerZBound = ourLowerLeftPosition.z; // inclusive + int upperXBound = lowerXBound + 32 - 1; // inclusive + int upperZBound = lowerZBound + 32 - 1; // inclusive + + // read mojang oversized data + for (File regionFile : regionFiles) { + ChunkCoordIntPair oversizedCoords = getOversizedChunkPair(regionFile); + if (oversizedCoords == null) { + continue; + } + + if ((oversizedCoords.x < lowerXBound || oversizedCoords.x > upperXBound) || (oversizedCoords.z < lowerZBound || oversizedCoords.z > upperZBound)) { + continue; // not in our regionfile + } + + // ensure oversized data is valid & is newer than data in the regionfile + + int location = (oversizedCoords.x & 31) | ((oversizedCoords.z & 31) << 5); + + byte[] chunkData; + try { + chunkData = Files.readAllBytes(regionFile.toPath()); + } catch (Exception ex) { + MinecraftServer.LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", data will be lost", ex); + continue; + } + + NBTTagCompound compound = null; + + // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them + RegionFileCompression compression = null; + for (RegionFileCompression compressionType : RegionFileCompression.getCompressionTypes().values()) { + try { + DataInputStream in = new DataInputStream(new BufferedInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData)))); // typical java + compound = NBTCompressedStreamTools.readNBT((DataInput)in); + compression = compressionType; + break; // reaches here iff readNBT does not throw + } catch (Exception ex) { + continue; + } + } + + if (compound == null) { + MinecraftServer.LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", it's corrupt. Its data will be lost"); + continue; + } + + if (compounds[location] == null || ChunkRegionLoader.getLastWorldSaveTime(compound) > ChunkRegionLoader.getLastWorldSaveTime(compounds[location])) { + oversized[location] = true; + oversizedCompressionTypes[location] = compression; + } + } + } + } + + // now we need to calculate a new offset header + + int[] calculatedOffsets = new int[32 * 32]; + RegionFileBitSet newSectorAllocations = new RegionFileBitSet(); + newSectorAllocations.allocate(0, 2); // make space for header + + // allocate sectors for normal chunks + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + if (oversized[location]) { + continue; + } + + int rawLength = rawLengths[location]; // bytes + int sectorOffset = sectorOffsets[location]; // sectors + int sectorLength = (int)roundToSectors(rawLength); + + if (newSectorAllocations.tryAllocate(sectorOffset, sectorLength)) { + calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized + } else { + MinecraftServer.LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.file.getAbsolutePath() + ", chunk will be regenerated"); + } + } + } + + // allocate sectors for oversized chunks + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + if (!oversized[location]) { + continue; + } + + int sectorOffset = newSectorAllocations.allocateNewSpace(1); + int sectorLength = 1; + + try { + this.dataFile.write(this.getOversizedChunkHolderData(oversizedCompressionTypes[location]), sectorOffset * 4096); + // only allocate in the new offsets if the write succeeds + calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized + } catch (IOException ex) { + newSectorAllocations.free(sectorOffset, sectorLength); + MinecraftServer.LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.file.getAbsolutePath() + " will be regenerated"); + } + } + } + + // rewrite aikar oversized data + + this.oversizedCount = 0; + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + int isAikarOversized = hasAikarOversized[location] ? 1 : 0; + + this.oversizedCount += isAikarOversized; + this.oversized[location] = (byte)isAikarOversized; + } + } + + if (this.oversizedCount > 0) { + try { + this.writeOversizedMeta(); + } catch (Exception ex) { + MinecraftServer.LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.file.getAbsolutePath(), ex); + this.getOversizedMetaFile().delete(); + } + } else { + this.getOversizedMetaFile().delete(); + } + + this.freeSectors.copyFrom(newSectorAllocations); + + // before we overwrite the old sectors, print a summary of the chunks that got changed. + + MinecraftServer.LOGGER.info("Starting summary of changes for regionfile " + this.file.getAbsolutePath()); + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + int oldOffset = this.getOffsets().get(location); + int newOffset = calculatedOffsets[location]; + + if (oldOffset == newOffset) { + continue; + } + + this.getOffsets().put(location, newOffset); // overwrite incorrect offset + + if (oldOffset == 0) { + // found lost data + MinecraftServer.LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.file.getAbsolutePath()); + } else if (newOffset == 0) { + MinecraftServer.LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.file.getAbsolutePath() + ", it will be regenerated"); + } else { + MinecraftServer.LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.file.getAbsolutePath()); + } + } + } + + MinecraftServer.LOGGER.info("End of change summary for regionfile " + this.file.getAbsolutePath()); + + // simply destroy the timestamp header, it's not used + + for (int i = 0; i < 32 * 32; ++i) { + this.getTimestamps().put(i, calculatedOffsets[i] != 0 ? (int)System.currentTimeMillis() : 0); // write a valid timestamp for valid chunks, I do not want to find out whatever dumb program actually checks this + } + + // write new header + try { + this.flushHeader(); + this.dataFile.force(true); // try to ensure it goes through... + MinecraftServer.LOGGER.info("Successfully wrote new header to disk for regionfile " + this.file.getAbsolutePath()); + } catch (IOException ex) { + MinecraftServer.LOGGER.fatal("Failed to write new header to disk for regionfile " + this.file.getAbsolutePath(), ex); + } + } + } + + final boolean canRecalcHeader; // final forces compile fail on new constructor + // Tuinity end + public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper // Paper start - Cache chunk status @@ -65,11 +401,22 @@ public class RegionFile implements AutoCloseable { // Paper end public RegionFile(File file, File file1, boolean flag) throws IOException { + // Tuinity start - add can recalc flag this(file.toPath(), file1.toPath(), RegionFileCompression.b, flag); } + public RegionFile(File file, File file1, boolean flag, boolean canRecalcHeader) throws IOException { + this(file.toPath(), file1.toPath(), RegionFileCompression.b, flag, canRecalcHeader); + // Tuinity end - add can recalc flag + } public RegionFile(java.nio.file.Path java_nio_file_path, java.nio.file.Path java_nio_file_path1, RegionFileCompression regionfilecompression, boolean flag) throws IOException { + // Tuinity start - add can recalc flag + this(java_nio_file_path, java_nio_file_path1, regionfilecompression, flag, false); + } + public RegionFile(java.nio.file.Path java_nio_file_path, java.nio.file.Path java_nio_file_path1, RegionFileCompression regionfilecompression, boolean flag, boolean canRecalcHeader) throws IOException { this.g = ByteBuffer.allocateDirect(8192); + this.canRecalcHeader = canRecalcHeader; + // Tuinity end - add can recalc flag this.file = java_nio_file_path.toFile(); // Paper initOversizedState(); // Paper this.freeSectors = new RegionFileBitSet(); @@ -97,14 +444,16 @@ public class RegionFile implements AutoCloseable { RegionFile.LOGGER.warn("Region file {} has truncated header: {}", java_nio_file_path, i); } - long j = Files.size(java_nio_file_path); + final long j = Files.size(java_nio_file_path); final long regionFileSize = j; + boolean needsHeaderRecalc = false; // Tuinity - recalculate header on header corruption + boolean hasBackedUp = false; // Tuinity - recalculate header on header corruption for (int k = 0; k < 1024; ++k) { - int l = this.h.get(k); + int l = this.h.get(k); final int headerLocation = k; // Tuinity - we expect this to be the header location if (l != 0) { - int i1 = b(l); - int j1 = a(l); + final int i1 = b(l); final int offset = i1; // Tuinity - we expect this to be offset in file in sectors + int j1 = a(l); final int sectorLength; // Tuinity - diff on change, we expect this to be sector length of region - watch out for reassignments // Spigot start if (j1 == 255) { // We're maxed out, so we need to read the proper length from the section @@ -112,33 +461,105 @@ public class RegionFile implements AutoCloseable { this.dataFile.read(realLen, i1 * 4096); j1 = (realLen.getInt(0) + 4) / 4096 + 1; } + sectorLength = j1; // Tuinity - diff on change, we expect this to be sector length of region // Spigot end if (i1 < 2) { RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", java_nio_file_path, k, i1); - this.h.put(k, 0); - } else if (j1 == 0) { + //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change + } else if (j1 <= 0) { // Tuinity - <= 0, not == RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", java_nio_file_path, k); - this.h.put(k, 0); + //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change } else if ((long) i1 * 4096L > j) { RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", java_nio_file_path, k, i1); - this.h.put(k, 0); + //this.h.put(k, 0); // Tuinity - we catch this, but need it in the header for the summary change } else { - this.freeSectors.a(i1, j1); + //this.freeSectors.a(i1, j1); // Tuinity - move this down so we can check if it fails to allocate + } + // Tuinity start - recalculate header on header corruption + if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) { + if (canRecalcHeader) { + MinecraftServer.LOGGER.error("Detected invalid header for regionfile " + this.file.getAbsolutePath() + "! Recalculating header..."); + needsHeaderRecalc = true; + break; + } else { + // location = chunkX | (chunkZ << 5); + MinecraftServer.LOGGER.fatal("Detected invalid header for regionfile " + this.file.getAbsolutePath() + + "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); + if (!hasBackedUp) { + hasBackedUp = true; + this.backupRegionFile(); + } + this.getTimestamps().put(headerLocation, 0); // be consistent, delete the timestamp too + this.getOffsets().put(headerLocation, 0); // delete the entry from header + continue; + } + } + boolean failedToAllocate = !this.freeSectors.tryAllocate(offset, sectorLength); + if (failedToAllocate) { + MinecraftServer.LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.file.getAbsolutePath()); } + if (failedToAllocate & !canRecalcHeader) { + // location = chunkX | (chunkZ << 5); + MinecraftServer.LOGGER.fatal("Detected invalid header for regionfile " + this.file.getAbsolutePath() + + "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); + if (!hasBackedUp) { + hasBackedUp = true; + this.backupRegionFile(); + } + this.getTimestamps().put(headerLocation, 0); // be consistent, delete the timestamp too + this.getOffsets().put(headerLocation, 0); // delete the entry from header + continue; + } + needsHeaderRecalc |= failedToAllocate; + // Tuinity end - recalculate header on header corruption } } + + // Tuinity start - recalculate header on header corruption + // we move the recalc here so comparison to old header is correct when logging to console + if (needsHeaderRecalc) { // true if header gave us overlapping allocations or had other issues + MinecraftServer.LOGGER.error("Recalculating regionfile " + this.file.getAbsolutePath() + ", header gave erroneous offsets & locations"); + this.recalculateHeader(); + } + // Tuinity end } } } + private final java.nio.file.Path getOversizedChunkPath(ChunkCoordIntPair chunkcoordintpair) { return this.e(chunkcoordintpair); } // Tuinity - OBFHELPER private java.nio.file.Path e(ChunkCoordIntPair chunkcoordintpair) { - String s = "c." + chunkcoordintpair.x + "." + chunkcoordintpair.z + ".mcc"; + String s = "c." + chunkcoordintpair.x + "." + chunkcoordintpair.z + ".mcc"; // Tuinity - diff on change return this.e.resolve(s); } + // Tuinity start + private static ChunkCoordIntPair getOversizedChunkPair(File file) { + String fileName = file.getName(); + + if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) { + return null; + } + + String[] split = fileName.split("\\."); + + if (split.length != 4) { + return null; + } + + try { + int x = Integer.parseInt(split[1]); + int z = Integer.parseInt(split[2]); + + return new ChunkCoordIntPair(x, z); + } catch (NumberFormatException ex) { + return null; + } + } + // Tuinity end + @Nullable public synchronized DataInputStream getReadStream(ChunkCoordIntPair chunkCoordIntPair) throws IOException { return a(chunkCoordIntPair);} // Paper - OBFHELPER @Nullable public synchronized DataInputStream a(ChunkCoordIntPair chunkcoordintpair) throws IOException { @@ -163,6 +584,12 @@ public class RegionFile implements AutoCloseable { ((java.nio.Buffer) bytebuffer).flip(); if (bytebuffer.remaining() < 5) { RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", chunkcoordintpair, l, bytebuffer.remaining()); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + // Tuinity end return null; } else { int i1 = bytebuffer.getInt(); @@ -170,6 +597,12 @@ public class RegionFile implements AutoCloseable { if (i1 == 0) { RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", chunkcoordintpair); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + // Tuinity end - recalculate header on regionfile corruption return null; } else { int j1 = i1 - 1; @@ -177,17 +610,49 @@ public class RegionFile implements AutoCloseable { if (a(b0)) { if (j1 != 0) { RegionFile.LOGGER.warn("Chunk has both internal and external streams"); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + // Tuinity end - recalculate header on regionfile corruption } - return this.a(chunkcoordintpair, b(b0)); + // Tuinity start - recalculate header on regionfile corruption + DataInputStream ret = this.a(chunkcoordintpair, b(b0)); + if (ret == null && this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + return ret; + // Tuinity end - recalculate header on regionfile corruption } else if (j1 > bytebuffer.remaining()) { RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", chunkcoordintpair, j1, bytebuffer.remaining()); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + // Tuinity end return null; } else if (j1 < 0) { RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, chunkcoordintpair); + // Tuinity start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + // Tuinity end - recalculate header on regionfile corruption return null; } else { - return this.a(chunkcoordintpair, b0, a(bytebuffer, j1)); + // Tuinity start - recalculate header on regionfile corruption + DataInputStream ret = this.a(chunkcoordintpair, b0, a(bytebuffer, j1)); + if (ret == null && this.canRecalcHeader) { + this.recalculateHeader(); + return this.getReadStream(chunkcoordintpair); + } + return ret; + // Tuinity end - recalculate header on regionfile corruption } } } @@ -347,10 +812,15 @@ public class RegionFile implements AutoCloseable { } private ByteBuffer b() { + // Tuinity start - add compressionType param + return this.getOversizedChunkHolderData(this.getRegionFileCompression()); + } + private ByteBuffer getOversizedChunkHolderData(RegionFileCompression compressionType) { + // Tuinity end ByteBuffer bytebuffer = ByteBuffer.allocate(5); bytebuffer.putInt(1); - bytebuffer.put((byte) (this.f.a() | 128)); + bytebuffer.put((byte) (compressionType.compressionTypeId() | 128)); // Tuinity - replace with compressionType ((java.nio.Buffer) bytebuffer).flip(); return bytebuffer; } @@ -387,6 +857,7 @@ public class RegionFile implements AutoCloseable { }; } + private final void flushHeader() throws IOException { this.b(); } // Tuinity - OBFHELPER private void c() throws IOException { ((java.nio.Buffer) this.g).position(0); this.dataFile.write(this.g, 0L); diff --git a/src/main/java/net/minecraft/server/RegionFileBitSet.java b/src/main/java/net/minecraft/server/RegionFileBitSet.java index 1ebdf73cc927405bc536dc74a5118d2a086db0e5..cfa3ecb031b59ec677f016ecdea92d16436fb511 100644 --- a/src/main/java/net/minecraft/server/RegionFileBitSet.java +++ b/src/main/java/net/minecraft/server/RegionFileBitSet.java @@ -4,18 +4,42 @@ import java.util.BitSet; public class RegionFileBitSet { - private final BitSet a = new BitSet(); + private final BitSet a = new BitSet(); private final BitSet getBitset() { return this.a; } // Tuinity - OBFHELPER public RegionFileBitSet() {} + public final void allocate(int from, int length) { this.a(from, length); } // Tuinity - OBFHELPER public void a(int i, int j) { this.a.set(i, i + j); } + public final void free(int from, int length) { this.b(from, length); } // Tuinity - OBFHELPER public void b(int i, int j) { this.a.clear(i, i + j); } + // Tuinity start + public final void copyFrom(RegionFileBitSet other) { + BitSet thisBitset = this.getBitset(); + BitSet otherBitset = other.getBitset(); + + for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) { + thisBitset.set(i, otherBitset.get(i)); + } + } + + public final boolean tryAllocate(int from, int length) { + BitSet bitset = this.getBitset(); + int firstSet = bitset.nextSetBit(from); + if (firstSet > 0 && firstSet < (from + length)) { + return false; + } + bitset.set(from, from + length); + return true; + } + // Tuinity end + + public final int allocateNewSpace(final int requiredLength) { return this.a(requiredLength); } // Tuinity - OBFHELPER public int a(int i) { int j = 0; diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java index d64f7ad925e5f40740a58ceee0845ac2db5419f2..8b341c14e7082fc96a464f2386a3dedea31ec59c 100644 --- a/src/main/java/net/minecraft/server/RegionFileCache.java +++ b/src/main/java/net/minecraft/server/RegionFileCache.java @@ -15,12 +15,43 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final public final Long2ObjectLinkedOpenHashMap cache = new Long2ObjectLinkedOpenHashMap(); private final File b; private final boolean c; + private final boolean isChunkData; // Tuinity RegionFileCache(File file, boolean flag) { + // Tuinity start - add isChunkData param + this(file, flag, false); + } + RegionFileCache(File file, boolean flag, boolean isChunkData) { + this.isChunkData = isChunkData; + // Tuinity end - add isChunkData param this.b = file; this.c = flag; } + // Tuinity start + public static ChunkCoordIntPair getRegionFileCoordinates(File file) { + String fileName = file.getName(); + if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { + return null; + } + + String[] split = fileName.split("\\."); + + if (split.length != 4) { + return null; + } + + try { + int x = Integer.parseInt(split[1]); + int z = Integer.parseInt(split[2]); + + return new ChunkCoordIntPair(x << 5, z << 5); + } catch (NumberFormatException ex) { + return null; + } + } + // Tuinity end + // Paper start public synchronized RegionFile getRegionFileIfLoaded(ChunkCoordIntPair chunkcoordintpair) { // Paper - synchronize for async io @@ -54,9 +85,9 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final this.b.mkdirs(); } - File file = new File(this.b, "r." + chunkcoordintpair.getRegionX() + "." + chunkcoordintpair.getRegionZ() + ".mca"); + File file = new File(this.b, "r." + chunkcoordintpair.getRegionX() + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Tuinity - diff on change if (existingOnly && !file.exists()) return null; // CraftBukkit - RegionFile regionfile1 = new RegionFile(file, this.b, this.c); + RegionFile regionfile1 = new RegionFile(file, this.b, this.c, this.isChunkData); // Tuinity - allow for chunk regionfiles to regen header this.cache.putAndMoveToFirst(i, regionfile1); // Paper start @@ -145,6 +176,13 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final return null; } // CraftBukkit end + // Tuinity start - Add regionfile parameter + return this.readFromRegionFile(regionfile, chunkcoordintpair); + } + private NBTTagCompound readFromRegionFile(RegionFile regionfile, ChunkCoordIntPair chunkcoordintpair) throws IOException { + // We add the regionfile parameter to avoid the potential deadlock (on fileLock) if we went back to obtain a regionfile + // if we decide to re-read + // Tuinity end try { // Paper DataInputStream datainputstream = regionfile.a(chunkcoordintpair); // Paper start @@ -160,6 +198,17 @@ public class RegionFileCache implements AutoCloseable { // Paper - no final try { if (datainputstream != null) { nbttagcompound = NBTCompressedStreamTools.a((DataInput) datainputstream); + // Tuinity start - recover from corrupt regionfile header + if (this.isChunkData) { + ChunkCoordIntPair chunkPos = ChunkRegionLoader.getChunkCoordinate(nbttagcompound); + if (!chunkPos.equals(chunkcoordintpair)) { + MinecraftServer.LOGGER.error("Attempting to read chunk data at " + chunkcoordintpair.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.file.getAbsolutePath()); + regionfile.recalculateHeader(); + regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. + return this.readFromRegionFile(regionfile, chunkcoordintpair); + } + } + // Tuinity end - recover from corrupt regionfile header return nbttagcompound; } diff --git a/src/main/java/net/minecraft/server/RegionFileCompression.java b/src/main/java/net/minecraft/server/RegionFileCompression.java index 3382d678e68e559b8d3cb9dced4fce24206cd38f..3b7894256dc8daa81be35f845cb5f8de02d7cb00 100644 --- a/src/main/java/net/minecraft/server/RegionFileCompression.java +++ b/src/main/java/net/minecraft/server/RegionFileCompression.java @@ -13,7 +13,7 @@ import javax.annotation.Nullable; public class RegionFileCompression { - private static final Int2ObjectMap d = new Int2ObjectOpenHashMap(); + private static final Int2ObjectMap d = new Int2ObjectOpenHashMap(); static final Int2ObjectMap getCompressionTypes() { return RegionFileCompression.d; } // Tuinity - OBFHELPER public static final RegionFileCompression a = a(new RegionFileCompression(1, GZIPInputStream::new, GZIPOutputStream::new)); public static final RegionFileCompression b = a(new RegionFileCompression(2, InflaterInputStream::new, DeflaterOutputStream::new)); public static final RegionFileCompression c = a(new RegionFileCompression(3, (inputstream) -> { @@ -36,8 +36,8 @@ public class RegionFileCompression { return regionfilecompression; } - @Nullable - public static RegionFileCompression a(int i) { + @Nullable public static RegionFileCompression getByType(int type) { return RegionFileCompression.a(type); } // Tuinity - OBFHELPER + @Nullable public static RegionFileCompression a(int i) { // Tuinity - OBFHELPER return (RegionFileCompression) RegionFileCompression.d.get(i); } @@ -45,6 +45,7 @@ public class RegionFileCompression { return RegionFileCompression.d.containsKey(i); } + public final int compressionTypeId() { return this.a(); } // Tuinity - OBFHELPER public int a() { return this.e; } @@ -53,6 +54,7 @@ public class RegionFileCompression { return (OutputStream) this.g.wrap(outputstream); } + public final InputStream wrap(InputStream inputstream) throws IOException { return this.a(inputstream); } // Tuinity - OBFHELPER public InputStream a(InputStream inputstream) throws IOException { return (InputStream) this.f.wrap(inputstream); } diff --git a/src/main/java/net/minecraft/server/RegionFileSection.java b/src/main/java/net/minecraft/server/RegionFileSection.java index 04256a95108b8182e8f808e856e0d2b62165e242..79a11d17a2822b192dec5981d0344ae689c3d385 100644 --- a/src/main/java/net/minecraft/server/RegionFileSection.java +++ b/src/main/java/net/minecraft/server/RegionFileSection.java @@ -25,8 +25,8 @@ public class RegionFileSection extends RegionFileCache implements AutoCloseab private static final Logger LOGGER = LogManager.getLogger(); // Paper - nuke IOWorker - private final Long2ObjectMap> c = new Long2ObjectOpenHashMap(); - protected final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); // Paper - private -> protected + private final Long2ObjectMap> c = new Long2ObjectOpenHashMap(); protected final Long2ObjectMap> getDataBySection() { return this.c; } // Tuinity - OBFHELPER + protected final LongLinkedOpenHashSet d = new LongLinkedOpenHashSet(); protected final LongLinkedOpenHashSet getDirtySections() { return this.d; } // Paper - private -> protected // Tuinity - OBFHELPER private final Function> e; private final Function f; private final DataFixer g; @@ -50,8 +50,42 @@ public class RegionFileSection extends RegionFileCache implements AutoCloseab } - @Nullable - protected Optional c(long i) { + // Tuinity start - actually unload POI data + public void unloadData(long coordinate) { + ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate); + this.writeDirtyData(chunkPos); + + Long2ObjectMap> data = this.getDataBySection(); + int before = data.size(); + + for (int section = 0; section < 16; ++section) { + data.remove(SectionPosition.asLong(chunkPos.x, section, chunkPos.z)); + } + + if (before != data.size()) { + this.onUnload(coordinate); + } + } + + protected void onUnload(long coordinate) {} + + public boolean isEmpty(long coordinate) { + Long2ObjectMap> data = this.getDataBySection(); + int x = MCUtil.getCoordinateX(coordinate); + int z = MCUtil.getCoordinateZ(coordinate); + for (int section = 0; section < 16; ++section) { + Optional optional = data.get(SectionPosition.asLong(x, section, z)); + if (optional != null && optional.orElse(null) != null) { + return false; + } + } + + return true; + } + // Tuinity end - actually unload POI data + + @Nullable protected Optional getIfLoaded(long value) { return this.c(value); } // Tuinity - OBFHELPER + @Nullable protected Optional c(long i) { // Tuinity - OBFHELPER return (Optional) this.c.get(i); } @@ -150,6 +184,7 @@ public class RegionFileSection extends RegionFileCache implements AutoCloseab }); } } + if (this instanceof VillagePlace) { ((VillagePlace)this).queueUnload(chunkcoordintpair.pair(), MinecraftServer.currentTickLong + 1); } // Tuinity - unload POI data } @@ -221,6 +256,7 @@ public class RegionFileSection extends RegionFileCache implements AutoCloseab return dynamic.get("DataVersion").asInt(1945); } + public final void writeDirtyData(ChunkCoordIntPair chunkcoordintpair) { this.a(chunkcoordintpair); } // Tuinity - OBFHELPER public void a(ChunkCoordIntPair chunkcoordintpair) { if (!this.d.isEmpty()) { for (int i = 0; i < 16; ++i) { diff --git a/src/main/java/net/minecraft/server/SectionPosition.java b/src/main/java/net/minecraft/server/SectionPosition.java index f95925f1c5d091f1a129d0437bb6e175c6ac080f..0bb3ad0bffc04eba38cd827eaf5c63e8bf2aee93 100644 --- a/src/main/java/net/minecraft/server/SectionPosition.java +++ b/src/main/java/net/minecraft/server/SectionPosition.java @@ -7,7 +7,7 @@ import java.util.stream.StreamSupport; public class SectionPosition extends BaseBlockPosition { - private SectionPosition(int i, int j, int k) { + public SectionPosition(int i, int j, int k) { // Tuinity - private -> public super(i, j, k); } diff --git a/src/main/java/net/minecraft/server/SensorNearestBed.java b/src/main/java/net/minecraft/server/SensorNearestBed.java index ad3609f2b884f64f1a1a449036cece49a46e933e..d3d28f97f9d2f969a182aec5e0947b6969d2939c 100644 --- a/src/main/java/net/minecraft/server/SensorNearestBed.java +++ b/src/main/java/net/minecraft/server/SensorNearestBed.java @@ -40,15 +40,15 @@ public class SensorNearestBed extends Sensor { return true; } }; - Stream stream = villageplace.a(VillagePlaceType.r.c(), predicate, entityinsentient.getChunkCoordinates(), 48, VillagePlace.Occupancy.ANY); - PathEntity pathentity = entityinsentient.getNavigation().a(stream, VillagePlaceType.r.d()); + Set set = BehaviorFindPosition.findPoi(villageplace, VillagePlaceType.r.c(), predicate, entityinsentient.getChunkCoordinates(), 48, VillagePlace.Occupancy.ANY, Integer.MAX_VALUE); // Tuinity - remove streams + PathEntity pathentity = entityinsentient.getNavigation().a(set, 8, false, VillagePlaceType.r.d()); // this.a((Set) stream.collect(Collectors.toSet()), 8, false, i) // Tuinity - remove streams if (pathentity != null && pathentity.j()) { BlockPosition blockposition = pathentity.m(); Optional optional = villageplace.c(blockposition); if (optional.isPresent()) { - entityinsentient.getBehaviorController().setMemory(MemoryModuleType.NEAREST_BED, (Object) blockposition); + entityinsentient.getBehaviorController().setMemory(MemoryModuleType.NEAREST_BED, blockposition); // Tuinity - decompile fix } } else if (this.b < 5) { this.a.long2LongEntrySet().removeIf((entry) -> { diff --git a/src/main/java/net/minecraft/server/SensorNearestItems.java b/src/main/java/net/minecraft/server/SensorNearestItems.java index 2e747158d48ab28ac1611990cc97aa4a9e30b30e..1de170b9fe6f2888da6dcf0151aaf1f865691c6a 100644 --- a/src/main/java/net/minecraft/server/SensorNearestItems.java +++ b/src/main/java/net/minecraft/server/SensorNearestItems.java @@ -18,20 +18,23 @@ public class SensorNearestItems extends Sensor { protected void a(WorldServer worldserver, EntityInsentient entityinsentient) { BehaviorController behaviorcontroller = entityinsentient.getBehaviorController(); - List list = worldserver.a(EntityItem.class, entityinsentient.getBoundingBox().grow(8.0D, 4.0D, 8.0D), (entityitem) -> { - return true; + // Tuinity start - remove streams + List list = worldserver.a(EntityItem.class, entityinsentient.getBoundingBox().grow(8.0D, 4.0D, 8.0D), (EntityItem item) -> { + return entityinsentient.i(item.getItemStack()) && item.a((Entity)entityinsentient, 9.0D); // copied from removed code, make sure to update - move here so we sort less }); - entityinsentient.getClass(); - list.sort(Comparator.comparingDouble(entityinsentient::h)); - Stream stream = list.stream().filter((entityitem) -> { - return entityinsentient.i(entityitem.getItemStack()); - }).filter((entityitem) -> { - return entityitem.a((Entity) entityinsentient, 9.0D); - }); - - entityinsentient.getClass(); - Optional optional = stream.filter(entityinsentient::hasLineOfSight).findFirst(); + list.sort(Comparator.comparingDouble(entityinsentient::h)); // better to take the sort perf hit than using line of sight more than we need to. + EntityItem nearest = null; + for (int index = 0, len = list.size(); index < len; ++index) { + EntityItem item = list.get(index); + if (entityinsentient.hasLineOfSight(item)) { + nearest = item; + break; + } + } + + Optional optional = Optional.ofNullable(nearest); + // Tuinity end - remove streams behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_WANTED_ITEM, optional); } diff --git a/src/main/java/net/minecraft/server/SensorNearestLivingEntities.java b/src/main/java/net/minecraft/server/SensorNearestLivingEntities.java index f6568a54ab85bc3a682f6fbb19dda7a783625bbe..4005df5ef3dec956a54feff539db2e63c226059a 100644 --- a/src/main/java/net/minecraft/server/SensorNearestLivingEntities.java +++ b/src/main/java/net/minecraft/server/SensorNearestLivingEntities.java @@ -21,10 +21,17 @@ public class SensorNearestLivingEntities extends Sensor { list.sort(Comparator.comparingDouble(entityliving::h)); BehaviorController behaviorcontroller = entityliving.getBehaviorController(); - behaviorcontroller.setMemory(MemoryModuleType.MOBS, (Object) list); - behaviorcontroller.setMemory(MemoryModuleType.VISIBLE_MOBS, list.stream().filter((entityliving1) -> { - return a(entityliving, entityliving1); - }).collect(Collectors.toList())); + behaviorcontroller.setMemory(MemoryModuleType.MOBS, list); // Tuinity - decompile fix + // Tuinity start - remove streams + List visible = new java.util.ArrayList<>(list.size()); + for (int index = 0, len = list.size(); index < len; ++index) { + EntityLiving nearby = list.get(index); + if (Sensor.a(entityliving, nearby)) { // copied from removed code, make sure to update + visible.add(nearby); + } + } + behaviorcontroller.setMemory(MemoryModuleType.VISIBLE_MOBS, visible); + // Tuinity end - remove streams } @Override diff --git a/src/main/java/net/minecraft/server/SensorNearestPlayers.java b/src/main/java/net/minecraft/server/SensorNearestPlayers.java index 904a6d5ac61d2ac81f1057068383e9ab432852db..c8e43a9f2a23178fdef52375b7204b90b28ac20b 100644 --- a/src/main/java/net/minecraft/server/SensorNearestPlayers.java +++ b/src/main/java/net/minecraft/server/SensorNearestPlayers.java @@ -19,22 +19,30 @@ public class SensorNearestPlayers extends Sensor { @Override protected void a(WorldServer worldserver, EntityLiving entityliving) { - Stream stream = worldserver.getPlayers().stream().filter(IEntitySelector.g).filter((entityplayer) -> { - return entityliving.a((Entity) entityplayer, 16.0D); - }); - - entityliving.getClass(); - List list = (List) stream.sorted(Comparator.comparingDouble(entityliving::h)).collect(Collectors.toList()); + // Tuinity start - remove streams + List nearby = (List)worldserver.getNearbyPlayers(entityliving, 16.0, IEntitySelector.g); + nearby.sort((e1, e2) -> Double.compare(entityliving.getDistanceSquared(e1), entityliving.getDistanceSquared(e2))); BehaviorController behaviorcontroller = entityliving.getBehaviorController(); - behaviorcontroller.setMemory(MemoryModuleType.NEAREST_PLAYERS, (Object) list); - List list1 = (List) list.stream().filter((entityhuman) -> { - return a(entityliving, (EntityLiving) entityhuman); - }).collect(Collectors.toList()); - - behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, (Object) (list1.isEmpty() ? null : (EntityHuman) list1.get(0))); - Optional optional = list1.stream().filter(IEntitySelector.f).findFirst(); - - behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_TARGETABLE_PLAYER, optional); + behaviorcontroller.setMemory(MemoryModuleType.NEAREST_PLAYERS, nearby); + EntityHuman first = null; + EntityHuman firstNonSpectator = null; + for (int index = 0, len = nearby.size(); index < len; ++index) { + EntityHuman entity = nearby.get(index); + if (!Sensor.a(entityliving, (EntityLiving)entity)) { // copied from removed code, make sure to update + continue; + } + if (first == null) { + first = entity; + } + if (IEntitySelector.f.test(entity)) { // copied from removed code, make sure to update + firstNonSpectator = entity; + break; + } + } + + behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_PLAYER, first); + behaviorcontroller.setMemory(MemoryModuleType.NEAREST_VISIBLE_TARGETABLE_PLAYER, Optional.ofNullable(firstNonSpectator)); + // Tuinity end - remove streams } } diff --git a/src/main/java/net/minecraft/server/SensorVillagerBabies.java b/src/main/java/net/minecraft/server/SensorVillagerBabies.java index a367bbfde4fbfeca6d01dec49c05f5e185aab43a..794b33a13b7f11b973caf085b0bded9b2135a4d7 100644 --- a/src/main/java/net/minecraft/server/SensorVillagerBabies.java +++ b/src/main/java/net/minecraft/server/SensorVillagerBabies.java @@ -17,11 +17,23 @@ public class SensorVillagerBabies extends Sensor { @Override protected void a(WorldServer worldserver, EntityLiving entityliving) { - entityliving.getBehaviorController().setMemory(MemoryModuleType.VISIBLE_VILLAGER_BABIES, (Object) this.a(entityliving)); + entityliving.getBehaviorController().setMemory(MemoryModuleType.VISIBLE_VILLAGER_BABIES, this.a(entityliving)); // Tuinity - decompile fix } private List a(EntityLiving entityliving) { - return (List) this.c(entityliving).stream().filter(this::b).collect(Collectors.toList()); + // Tuinity start - remove streams + List nearby = this.c(entityliving); // copied from removed code, make sure to update + List ret = new java.util.ArrayList<>(); + + for (int index = 0, len = nearby.size(); index < len; ++index) { + EntityLiving entity = nearby.get(index); + if (this.b(entity)) { // copied from removed code, make sure to update + ret.add(entity); + } + } + + return ret; + // Tuinity end - remove streams } private boolean b(EntityLiving entityliving) { diff --git a/src/main/java/net/minecraft/server/ServerConnection.java b/src/main/java/net/minecraft/server/ServerConnection.java index 5f4dacf9c93c2495a07df2647fe0411f796da6af..0668d383db1f3a81d1053954d72678c7ac5aecec 100644 --- a/src/main/java/net/minecraft/server/ServerConnection.java +++ b/src/main/java/net/minecraft/server/ServerConnection.java @@ -74,6 +74,11 @@ public class ServerConnection { ServerConnection.LOGGER.info("Using default channel type"); } + // Tuinity start - indicate Velocity natives in use + ServerConnection.LOGGER.info("Tuinity: Using " + com.velocitypowered.natives.util.Natives.compress.getLoadedVariant() + " compression from Velocity."); + ServerConnection.LOGGER.info("Tuinity: Using " + com.velocitypowered.natives.util.Natives.cipher.getLoadedVariant() + " cipher from Velocity."); + // Tuinity end + this.listeningChannels.add(((ServerBootstrap) ((ServerBootstrap) (new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer() { protected void initChannel(Channel channel) throws Exception { try { diff --git a/src/main/java/net/minecraft/server/StructureManager.java b/src/main/java/net/minecraft/server/StructureManager.java index f199368a6d78b0cd52f11ca2c8509d729b918852..2598ae3710d46c2cfd2be5d6be2a56e59ceef6ea 100644 --- a/src/main/java/net/minecraft/server/StructureManager.java +++ b/src/main/java/net/minecraft/server/StructureManager.java @@ -35,8 +35,13 @@ public class StructureManager { // Paper start - remove structure streams public java.util.List> getFeatureStarts(SectionPosition sectionPosition, StructureGenerator structureGenerator) { + // Tuinity start - add world parameter + return this.getFeatureStarts(sectionPosition, structureGenerator, null); + } + public java.util.List> getFeatureStarts(SectionPosition sectionPosition, StructureGenerator structureGenerator, IWorldReader world) { + // Tuinity end - add world parameter java.util.List> list = new ObjectArrayList<>(); - for (Long curLong: getLevel().getChunkAt(sectionPosition.a(), sectionPosition.c(), ChunkStatus.STRUCTURE_REFERENCES).b(structureGenerator)) { + for (Long curLong: (world == null ? getLevel() : world).getChunkAt(sectionPosition.a(), sectionPosition.c(), ChunkStatus.STRUCTURE_REFERENCES).b(structureGenerator)) { // Tuinity - fix deadlock on world gen - chunk can be unloaded while generating, so we should be using the generator's regionlimitedaccess so we always get the chunk SectionPosition sectionPosition1 = SectionPosition.a(new ChunkCoordIntPair(curLong), 0); StructureStart structurestart = a(sectionPosition1, structureGenerator, getLevel().getChunkAt(sectionPosition1.a(), sectionPosition1.c(), ChunkStatus.STRUCTURE_STARTS)); if (structurestart != null && structurestart.e()) { @@ -65,8 +70,12 @@ public class StructureManager { } public StructureStart a(BlockPosition blockposition, boolean flag, StructureGenerator structuregenerator) { + // Tuinity start - add world parameter + return this.getStructureStarts(blockposition,flag, structuregenerator, null); + } + public StructureStart getStructureStarts(BlockPosition blockposition, boolean flag, StructureGenerator structuregenerator, IWorldReader world) { // Paper start - remove structure streams - for (StructureStart structurestart : getFeatureStarts(SectionPosition.a(blockposition), structuregenerator)) { + for (StructureStart structurestart : getFeatureStarts(SectionPosition.a(blockposition), structuregenerator, world)) { // Tuinity end - add world parameter if (structurestart.c().b(blockposition)) { if (!flag) { return structurestart; diff --git a/src/main/java/net/minecraft/server/Ticket.java b/src/main/java/net/minecraft/server/Ticket.java index e41cb8613efc86499dfe3be36c9130ab6dc9b89e..c19ffb925a02d123da8a5c77186e6105422dccf7 100644 --- a/src/main/java/net/minecraft/server/Ticket.java +++ b/src/main/java/net/minecraft/server/Ticket.java @@ -5,17 +5,17 @@ import java.util.Objects; public final class Ticket implements Comparable> { private final TicketType a; - private final int b; + private int b; public final void setTicketLevel(final int value) { this.b = value; } // Tuinity - remove final, add set OBFHELPER public final T identifier; public final T getObjectReason() { return this.identifier; } // Paper - OBFHELPER - private long d; public final long getCreationTick() { return this.d; } // Paper - OBFHELPER + private long d; public final long getCreationTick() { return this.d; } public final void setCreationTick(final long value) { this.d = value; } // Paper - OBFHELPER // Tuinity - OBFHELPER public int priority = 0; // Paper - public long delayUnloadBy; // Paper + boolean isCached; // Tuinity - delay chunk unloads, this defends against really stupid plugins protected Ticket(TicketType tickettype, int i, T t0) { this.a = tickettype; this.b = i; this.identifier = t0; - this.delayUnloadBy = tickettype.loadPeriod; // Paper + // Tuinity - delay chunk unloads } public int compareTo(Ticket ticket) { @@ -64,8 +64,9 @@ public final class Ticket implements Comparable> { this.d = i; } + protected final boolean isExpired(long time) { return this.b(time); } // Tuinity - OBFHELPER protected boolean b(long i) { - long j = delayUnloadBy; // Paper + long j = this.a.b(); // Tuinity - delay chunk unloads return j != 0L && i - this.d > j; } diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java index 5c789b25f1df2eae8ea8ceb4ba977ba336fe6d5e..3c964f26592fc84bb5fc11c60808d11c65d93b16 100644 --- a/src/main/java/net/minecraft/server/TicketType.java +++ b/src/main/java/net/minecraft/server/TicketType.java @@ -26,8 +26,22 @@ public class TicketType { public static final TicketType ASYNC_LOAD = a("async_load", Long::compareTo); // Paper public static final TicketType PRIORITY = a("priority", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper public static final TicketType URGENT = a("urgent", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper - public static final TicketType DELAY_UNLOAD = a("delay_unload", Long::compareTo, 300); // Paper + public static final TicketType DELAYED_UNLOAD = a("delayed_unload", Long::compareTo); // Tuinity - delay chunk unloads + public static final TicketType REQUIRED_LOAD = a("required_load", Long::compareTo); // Tuinity - make sure getChunkAt does not fail + public static final TicketType LIGHT_UPDATE = a("light_update", Comparator.comparingLong(ChunkCoordIntPair::pair)); // Tuinity - ensure chunks stay loaded for lighting + public static final TicketType CHUNK_RELIGHT = a("chunk_relight", Long::compareTo); // Tuinity - ensure chunk stays loaded for relighting + // Tuinity start - delay chunk unloads + boolean delayUnloadViable = true; + static { + TicketType.LIGHT.delayUnloadViable = false; + TicketType.PLUGIN.delayUnloadViable = false; + TicketType.PRIORITY.delayUnloadViable = false; + TicketType.URGENT.delayUnloadViable = false; + TicketType.DELAYED_UNLOAD.delayUnloadViable = false; + TicketType.LIGHT_UPDATE.delayUnloadViable = false; // Tuinity - ensure chunks stay loaded for lighting + } + // Tuinity end - delay chunk unloads public static TicketType a(String s, Comparator comparator) { return new TicketType<>(s, comparator, 0L); } diff --git a/src/main/java/net/minecraft/server/UserCache.java b/src/main/java/net/minecraft/server/UserCache.java index 2484293b12d9ec88b8a2570aa853a12f0d858193..1496c43fc9487caf6ddb3782a9d1c79ef6ca1e94 100644 --- a/src/main/java/net/minecraft/server/UserCache.java +++ b/src/main/java/net/minecraft/server/UserCache.java @@ -49,6 +49,11 @@ public class UserCache { private final File g; private final AtomicLong h = new AtomicLong(); + // Tuinity start + protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock(); + protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock(); + // Tuinity end + public UserCache(GameProfileRepository gameprofilerepository, File file) { this.e = gameprofilerepository; this.g = file; @@ -56,6 +61,7 @@ public class UserCache { } private void a(UserCache.UserCacheEntry usercache_usercacheentry) { + try { this.stateLock.lock(); // Tuinity - allow better concurrency GameProfile gameprofile = usercache_usercacheentry.a(); usercache_usercacheentry.a(this.d()); @@ -70,6 +76,7 @@ public class UserCache { if (uuid != null) { this.d.put(uuid, usercache_usercacheentry); } + } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency } @@ -107,7 +114,7 @@ public class UserCache { } public void saveProfile(GameProfile gameprofile) { a(gameprofile); } // Paper - OBFHELPER - public synchronized void a(GameProfile gameprofile) { // Paper - synchronize + public void a(GameProfile gameprofile) { // Paper - synchronize // Tuinity - allow better concurrency Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date()); @@ -124,8 +131,9 @@ public class UserCache { } @Nullable - public synchronized GameProfile getProfile(String s) { // Paper - synchronize + public GameProfile getProfile(String s) { // Paper - synchronize // Tuinity start - allow better concurrency String s1 = s.toLowerCase(Locale.ROOT); + boolean stateLocked = true; try { this.stateLock.lock(); // Tuinity - allow better concurrency UserCache.UserCacheEntry usercache_usercacheentry = (UserCache.UserCacheEntry) this.c.get(s1); boolean flag = false; @@ -139,10 +147,14 @@ public class UserCache { GameProfile gameprofile; if (usercache_usercacheentry != null) { + stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency usercache_usercacheentry.a(this.d()); gameprofile = usercache_usercacheentry.a(); } else { + stateLocked = false; this.stateLock.unlock(); // Tuinity - allow better concurrency + try { this.lookupLock.lock(); // Tuinity - allow better concurrency gameprofile = a(this.e, s); // Spigot - use correct case for offline players + } finally { this.lookupLock.unlock(); } // Tuinity - allow better concurrency if (gameprofile != null) { this.a(gameprofile); flag = false; @@ -154,6 +166,7 @@ public class UserCache { } return gameprofile; + } finally { if (stateLocked) { this.stateLock.unlock(); } } // Tuinity - allow better concurrency } // Paper start @@ -287,7 +300,9 @@ public class UserCache { } private Stream a(int i) { + try { this.stateLock.lock(); // Tuinity - allow better concurrency return ImmutableList.copyOf(this.d.values()).stream().sorted(Comparator.comparing(UserCache.UserCacheEntry::c).reversed()).limit((long) i); + } finally { this.stateLock.unlock(); } // Tuinity - allow better concurrency } private static JsonElement a(UserCache.UserCacheEntry usercache_usercacheentry, DateFormat dateformat) { diff --git a/src/main/java/net/minecraft/server/Vec3D.java b/src/main/java/net/minecraft/server/Vec3D.java index 7f05587d42b7cdb09552277ec2e467f0edf06f10..5af554870bcf36e47aef43b966b141b9eda6c4d5 100644 --- a/src/main/java/net/minecraft/server/Vec3D.java +++ b/src/main/java/net/minecraft/server/Vec3D.java @@ -4,7 +4,7 @@ import java.util.EnumSet; public class Vec3D implements IPosition { - public static final Vec3D ORIGIN = new Vec3D(0.0D, 0.0D, 0.0D); + public static final Vec3D ORIGIN = new Vec3D(0.0D, 0.0D, 0.0D); public static Vec3D getZeroVector() { return Vec3D.ORIGIN; } // Tuinity - OBFHELPER public final double x; public final double y; public final double z; @@ -61,6 +61,7 @@ public class Vec3D implements IPosition { return this.add(-d0, -d1, -d2); } + public final Vec3D add(Vec3D vec3d) { return this.e(vec3d); } // Tuinity - OBFHELPER public Vec3D e(Vec3D vec3d) { return this.add(vec3d.x, vec3d.y, vec3d.z); } @@ -109,10 +110,12 @@ public class Vec3D implements IPosition { return new Vec3D(this.x * d0, this.y * d1, this.z * d2); } + public final double magnitude() { return this.f(); } // Tuinity - OBFHELPER public double f() { return (double) MathHelper.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } + public final double magnitudeSquared() { return this.g(); } // Tuinity - OBFHELPER public double g() { return this.x * this.x + this.y * this.y + this.z * this.z; } diff --git a/src/main/java/net/minecraft/server/VillagePlace.java b/src/main/java/net/minecraft/server/VillagePlace.java index 6a0f07b13eef5560dfc7c7b39618c0b825533aec..731a7b52c1c659b3b985704dea9cf57f6c5bcecb 100644 --- a/src/main/java/net/minecraft/server/VillagePlace.java +++ b/src/main/java/net/minecraft/server/VillagePlace.java @@ -4,6 +4,7 @@ import com.mojang.datafixers.DataFixer; import com.mojang.datafixers.util.Pair; import it.unimi.dsi.fastutil.longs.Long2ByteMap; import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; // Tuinity import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import java.io.File; @@ -22,8 +23,24 @@ import java.util.stream.Stream; public class VillagePlace extends RegionFileSection { - private final VillagePlace.a a = new VillagePlace.a(); - private final LongSet b = new LongOpenHashSet(); + // Tuinity start - unload poi data + // the vanilla tracker needs to be replaced because it does not support level removes + private final com.tuinity.tuinity.util.misc.Delayed26WayDistancePropagator3D villageDistanceTracker = new com.tuinity.tuinity.util.misc.Delayed26WayDistancePropagator3D(); + static final int POI_DATA_SOURCE = 7; + public static int convertBetweenLevels(final int level) { + return POI_DATA_SOURCE - level; + } + + protected void updateDistanceTracking(long section) { + if (this.isSectionDistanceTrackerSource(section)) { + this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); + } else { + this.villageDistanceTracker.removeSource(section); + } + } + // Tuinity end - unload poi data + + private final LongSet b = new LongOpenHashSet(); private final LongSet getLoadedChunks() { return this.b; } // Tuinity - OBFHELPER private final WorldServer world; // Paper @@ -34,9 +51,124 @@ public class VillagePlace extends RegionFileSection { public VillagePlace(File file, DataFixer datafixer, boolean flag, WorldServer world) { super(file, VillagePlaceSection::a, VillagePlaceSection::new, datafixer, DataFixTypes.POI_CHUNK, flag); this.world = world; + if (world == null) { throw new IllegalStateException("world must be non-null"); }// Tuinity - require non-null // Paper end - add world parameter } + // Tuinity start - actually unload POI data + private final java.util.TreeSet queuedUnloads = new java.util.TreeSet<>(); + private final Long2ObjectOpenHashMap queuedUnloadsByCoordinate = new Long2ObjectOpenHashMap<>(); + + static final class QueuedUnload implements Comparable { + + private final long unloadTick; + private final long coordinate; + + public QueuedUnload(long unloadTick, long coordinate) { + this.unloadTick = unloadTick; + this.coordinate = coordinate; + } + + @Override + public int compareTo(QueuedUnload other) { + if (other.unloadTick == this.unloadTick) { + return Long.compare(this.coordinate, other.coordinate); + } else { + return Long.compare(this.unloadTick, other.unloadTick); + } + } + + @Override + public int hashCode() { + int hash = 1; + hash = hash * 31 + Long.hashCode(this.unloadTick); + hash = hash * 31 + Long.hashCode(this.coordinate); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != QueuedUnload.class) { + return false; + } + QueuedUnload other = (QueuedUnload)obj; + return other.unloadTick == this.unloadTick && other.coordinate == this.coordinate; + } + } + + long determineDelay(long coordinate) { + if (this.isEmpty(coordinate)) { + return 60 * 20; + } else { + return 5 * 20; + } + } + + public void queueUnload(long coordinate, long minTarget) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload queue"); + QueuedUnload unload = new QueuedUnload(minTarget + this.determineDelay(coordinate), coordinate); + QueuedUnload existing = this.queuedUnloadsByCoordinate.put(coordinate, unload); + if (existing != null) { + this.queuedUnloads.remove(existing); + } + this.queuedUnloads.add(unload); + } + + public void dequeueUnload(long coordinate) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload dequeue"); + QueuedUnload unload = this.queuedUnloadsByCoordinate.remove(coordinate); + if (unload != null) { + this.queuedUnloads.remove(unload); + } + } + + public void pollUnloads(BooleanSupplier canSleepForTick) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload"); + long currentTick = MinecraftServer.currentTickLong; + ChunkProviderServer chunkProvider = this.world.getChunkProvider(); + PlayerChunkMap playerChunkMap = chunkProvider.playerChunkMap; + // copied target determination from PlayerChunkMap + int target = Math.min(this.queuedUnloads.size() - 100, (int) (this.queuedUnloads.size() * PlayerChunkMap.UNLOAD_QUEUE_RESIZE_FACTOR)); // Paper - Make more aggressive + for (java.util.Iterator iterator = this.queuedUnloads.iterator(); + iterator.hasNext() && (this.queuedUnloads.size() > target || canSleepForTick.getAsBoolean());) { + QueuedUnload unload = iterator.next(); + if (unload.unloadTick > currentTick) { + break; + } + + long coordinate = unload.coordinate; + + iterator.remove(); + this.queuedUnloadsByCoordinate.remove(coordinate); + + if (playerChunkMap.getUnloadingPlayerChunk(MCUtil.getCoordinateX(coordinate), MCUtil.getCoordinateZ(coordinate)) != null + || playerChunkMap.getUpdatingChunk(coordinate) != null) { + continue; + } + + this.unloadData(coordinate); + } + } + + @Override + public void unloadData(long coordinate) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async unloading poi data"); + super.unloadData(coordinate); + } + + @Override + protected void onUnload(long coordinate) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("async poi unload callback"); + this.getLoadedChunks().remove(coordinate); + int chunkX = MCUtil.getCoordinateX(coordinate); + int chunkZ = MCUtil.getCoordinateZ(coordinate); + for (int section = 0; section < 16; ++section) { + long sectionPos = SectionPosition.asLong(chunkX, section, chunkZ); + this.updateDistanceTracking(sectionPos); + } + } + // Tuinity end - actually unload POI data + public void a(BlockPosition blockposition, VillagePlaceType villageplacetype) { ((VillagePlaceSection) this.e(SectionPosition.a(blockposition).s())).a(blockposition, villageplacetype); } @@ -140,10 +272,11 @@ public class VillagePlace extends RegionFileSection { } public int a(SectionPosition sectionposition) { - this.a.a(); - return this.a.c(sectionposition.s()); + this.villageDistanceTracker.propagateUpdates(); // Tuinity - replace distance tracking util + return convertBetweenLevels(this.villageDistanceTracker.getLevel(MCUtil.getSectionKey(sectionposition))); // Tuinity - replace distance tracking util } + private boolean isSectionDistanceTrackerSource(long section) { return this.f(section); } // Tuinity - OBFHELPER private boolean f(long i) { Optional optional = this.c(i); @@ -159,7 +292,7 @@ public class VillagePlace extends RegionFileSection { super.a(booleansupplier); } else { //super.a(booleansupplier); // re-implement below - while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean()) { + while (!((RegionFileSection)this).d.isEmpty() && booleansupplier.getAsBoolean() && !this.world.isSavingDisabled()) { // Tuinity - unload POI data - don't write to disk if saving is disabled ChunkCoordIntPair chunkcoordintpair = SectionPosition.a(((RegionFileSection)this).d.firstLong()).r(); NBTTagCompound data; @@ -167,22 +300,27 @@ public class VillagePlace extends RegionFileSection { data = this.getData(chunkcoordintpair); } com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave(this.world, - chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.LOW_PRIORITY); + chunkcoordintpair.x, chunkcoordintpair.z, data, null, com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - use normal priority } } + // Tuinity start - unload POI data + if (!this.world.isSavingDisabled()) { // don't write to disk if saving is disabled + this.pollUnloads(booleansupplier); + } + // Tuinity end - unload POI data // Paper end - this.a.a(); + this.villageDistanceTracker.propagateUpdates(); // Tuinity - replace distance tracking until } @Override protected void a(long i) { super.a(i); - this.a.b(i, this.a.b(i), false); + this.updateDistanceTracking(i); // Tuinity - move to new distance tracking util } @Override protected void b(long i) { - this.a.b(i, this.a.b(i), false); + this.updateDistanceTracking(i); // Tuinity - move to new distance tracking util } public void a(ChunkCoordIntPair chunkcoordintpair, ChunkSection chunksection) { @@ -247,7 +385,7 @@ public class VillagePlace extends RegionFileSection { @Override protected int b(long i) { - return VillagePlace.this.f(i) ? 0 : 7; + return VillagePlace.this.f(i) ? 0 : 7; // Tuinity - unload poi data - diff on change, this specifies the source level to use for distance tracking } @Override @@ -292,7 +430,7 @@ public class VillagePlace extends RegionFileSection { if (this.world != null && Thread.currentThread() != com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE) { com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.scheduleSave( this.world, chunkcoordintpair.x, chunkcoordintpair.z, nbttagcompound, null, - com.destroystokyo.paper.io.IOUtil.getPriorityForCurrentThread()); + com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY); // Tuinity - writes are async, no need for priority return; } super.write(chunkcoordintpair, nbttagcompound); @@ -311,6 +449,7 @@ public class VillagePlace extends RegionFileSection { this.d = predicate; } + public final Predicate getPredicate() { return this.a(); } // Tuinity - OBFHELPER public Predicate a() { return this.d; } diff --git a/src/main/java/net/minecraft/server/VillagePlaceRecord.java b/src/main/java/net/minecraft/server/VillagePlaceRecord.java index 0b40c2f4dada7d8432e3f91e9cf206c2bda3b24b..6eaf9fc9cc93f79a497b07a3549d459ba66be849 100644 --- a/src/main/java/net/minecraft/server/VillagePlaceRecord.java +++ b/src/main/java/net/minecraft/server/VillagePlaceRecord.java @@ -6,7 +6,7 @@ import java.util.Objects; public class VillagePlaceRecord { - private final BlockPosition a; + private final BlockPosition a; public final BlockPosition getPosition() { return this.a; } // Tuinity - OBFHELPER private final VillagePlaceType b; private int c; private final Runnable d; diff --git a/src/main/java/net/minecraft/server/VillagePlaceSection.java b/src/main/java/net/minecraft/server/VillagePlaceSection.java index b86963aa34b5ae479f924c5a52afc5b5b66dba76..943a437ff27162eae09211c28bdc0d141fa6a404 100644 --- a/src/main/java/net/minecraft/server/VillagePlaceSection.java +++ b/src/main/java/net/minecraft/server/VillagePlaceSection.java @@ -23,12 +23,12 @@ public class VillagePlaceSection { private static final Logger LOGGER = LogManager.getLogger(); private final Short2ObjectMap b; - private final Map> c; + private final Map> c; public final Map> getData() { return this.c; } // Tuinity - OBFHELPER private final Runnable d; private boolean e; public static Codec a(Runnable runnable) { - Codec codec = RecordCodecBuilder.create((instance) -> { + Codec codec = RecordCodecBuilder.create((instance) -> { // Tuinity - decompile fix return instance.group(RecordCodecBuilder.point(runnable), Codec.BOOL.optionalFieldOf("Valid", false).forGetter((villageplacesection) -> { return villageplacesection.e; }), VillagePlaceRecord.a(runnable).listOf().fieldOf("Records").forGetter((villageplacesection) -> { diff --git a/src/main/java/net/minecraft/server/VoxelShape.java b/src/main/java/net/minecraft/server/VoxelShape.java index eb926b74e17fb2f88c1d6ce2fb546541f8e6e274..e3b72922e2dfad07f3452ec5ee2af379d968c52d 100644 --- a/src/main/java/net/minecraft/server/VoxelShape.java +++ b/src/main/java/net/minecraft/server/VoxelShape.java @@ -8,11 +8,11 @@ import javax.annotation.Nullable; public abstract class VoxelShape { - protected final VoxelShapeDiscrete a; + protected final VoxelShapeDiscrete a; public final VoxelShapeDiscrete getShape() { return this.a; } // Tuinity - OBFHELPER @Nullable private VoxelShape[] b; - VoxelShape(VoxelShapeDiscrete voxelshapediscrete) { + protected VoxelShape(VoxelShapeDiscrete voxelshapediscrete) { // Tuinity this.a = voxelshapediscrete; } @@ -48,9 +48,16 @@ public abstract class VoxelShape { public final VoxelShape offset(double x, double y, double z) { return this.a(x, y, z); } // Paper - OBFHELPER public VoxelShape a(double d0, double d1, double d2) { - return (VoxelShape) (this.isEmpty() ? VoxelShapes.a() : new VoxelShapeArray(this.a, new DoubleListOffset(this.a(EnumDirection.EnumAxis.X), d0), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Y), d1), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Z), d2))); + return (VoxelShape) (this.isEmpty() ? VoxelShapes.a() : new VoxelShapeArray(this.a, new DoubleListOffset(this.a(EnumDirection.EnumAxis.X), d0), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Y), d1), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Z), d2))); // Tuinity - diff on change, copied into VoxelShapeArray override } + // Tuinity start - optimise multi-aabb shapes + public boolean intersects(final AxisAlignedBB axisalingedbb) { + return VoxelShapes.applyOperation(this, new com.tuinity.tuinity.voxel.AABBVoxelShape(axisalingedbb), OperatorBoolean.AND); + } + // Tuinity end - optimise multi-aabb shapes + + public final VoxelShape simplify() { return this.c(); } // Tuinity - OBFHELPER public VoxelShape c() { VoxelShape[] avoxelshape = new VoxelShape[]{VoxelShapes.a()}; @@ -70,6 +77,7 @@ public abstract class VoxelShape { }, true); } + public final List getBoundingBoxesRepresentation() { return this.d(); } // Tuinity - OBFHELPER public List d() { List list = Lists.newArrayList(); diff --git a/src/main/java/net/minecraft/server/VoxelShapeArray.java b/src/main/java/net/minecraft/server/VoxelShapeArray.java index 3c29cb1452cde1308f630bfcb82876ef19057e8f..c14b7bd63e3917bc5f495655c40d8825a8d2062f 100644 --- a/src/main/java/net/minecraft/server/VoxelShapeArray.java +++ b/src/main/java/net/minecraft/server/VoxelShapeArray.java @@ -3,6 +3,7 @@ package net.minecraft.server; import it.unimi.dsi.fastutil.doubles.DoubleArrayList; import it.unimi.dsi.fastutil.doubles.DoubleList; import java.util.Arrays; +import java.util.List; public final class VoxelShapeArray extends VoxelShape { @@ -10,11 +11,25 @@ public final class VoxelShapeArray extends VoxelShape { private final DoubleList c; private final DoubleList d; + // Tuinity start - optimise multi-aabb shapes + static final AxisAlignedBB[] EMPTY = new AxisAlignedBB[0]; + final AxisAlignedBB[] boundingBoxesRepresentation; + + final double offsetX; + final double offsetY; + final double offsetZ; + // Tuinity end - optimise multi-aabb shapes + protected VoxelShapeArray(VoxelShapeDiscrete voxelshapediscrete, double[] adouble, double[] adouble1, double[] adouble2) { this(voxelshapediscrete, (DoubleList) DoubleArrayList.wrap(Arrays.copyOf(adouble, voxelshapediscrete.b() + 1)), (DoubleList) DoubleArrayList.wrap(Arrays.copyOf(adouble1, voxelshapediscrete.c() + 1)), (DoubleList) DoubleArrayList.wrap(Arrays.copyOf(adouble2, voxelshapediscrete.d() + 1))); } VoxelShapeArray(VoxelShapeDiscrete voxelshapediscrete, DoubleList doublelist, DoubleList doublelist1, DoubleList doublelist2) { + // Tuinity start - optimise multi-aabb shapes + this(voxelshapediscrete, doublelist, doublelist1, doublelist2, null, null, 0.0, 0.0, 0.0); + } + VoxelShapeArray(VoxelShapeDiscrete voxelshapediscrete, DoubleList doublelist, DoubleList doublelist1, DoubleList doublelist2, VoxelShapeArray original, AxisAlignedBB[] boundingBoxesRepresentation, double offsetX, double offsetY, double offsetZ) { + // Tuinity end - optimise multi-aabb shapes super(voxelshapediscrete); int i = voxelshapediscrete.b() + 1; int j = voxelshapediscrete.c() + 1; @@ -27,6 +42,18 @@ public final class VoxelShapeArray extends VoxelShape { } else { throw (IllegalArgumentException) SystemUtils.c((Throwable) (new IllegalArgumentException("Lengths of point arrays must be consistent with the size of the VoxelShape."))); } + // Tuinity start - optimise multi-aabb shapes + this.boundingBoxesRepresentation = boundingBoxesRepresentation == null ? this.getBoundingBoxesRepresentation().toArray(EMPTY) : boundingBoxesRepresentation; // Tuinity - optimise multi-aabb shapes + if (original == null) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } else { + this.offsetX = offsetX + original.offsetX; + this.offsetY = offsetY + original.offsetY; + this.offsetZ = offsetZ + original.offsetZ; + } + // Tuinity end - optimise multi-aabb shapes } @Override @@ -42,4 +69,63 @@ public final class VoxelShapeArray extends VoxelShape { throw new IllegalArgumentException(); } } + + // Tuinity start - optimise multi-aabb shapes + @Override + public VoxelShape a(double d0, double d1, double d2) { + if (this == VoxelShapes.getEmptyShape() || this.boundingBoxesRepresentation.length == 0) { + return this; + } + return new VoxelShapeArray(this.a, new DoubleListOffset(this.a(EnumDirection.EnumAxis.X), d0), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Y), d1), new DoubleListOffset(this.a(EnumDirection.EnumAxis.Z), d2), this, this.boundingBoxesRepresentation, d0, d1, d2); + } + + @Override + public List d() { // getBoundingBoxesRepresentation + if (this.boundingBoxesRepresentation == null) { + return super.d(); + } + List ret = new java.util.ArrayList<>(this.boundingBoxesRepresentation.length); + + double offX = this.offsetX; + double offY = this.offsetY; + double offZ = this.offsetZ; + for (AxisAlignedBB boundingBox : this.boundingBoxesRepresentation) { + ret.add(boundingBox.offset(offX, offY, offZ)); + } + + return ret; + } + + public final AxisAlignedBB[] getBoundingBoxesRepresentationRaw() { + return this.boundingBoxesRepresentation; + } + + public final double getOffsetX() { + return this.offsetX; + } + + public final double getOffsetY() { + return this.offsetY; + } + + public final double getOffsetZ() { + return this.offsetZ; + } + + public final boolean intersects(AxisAlignedBB axisalingedbb) { + // this can be optimised by checking an "overall shape" first, but not needed + double offX = this.offsetX; + double offY = this.offsetY; + double offZ = this.offsetZ; + + for (AxisAlignedBB boundingBox : this.boundingBoxesRepresentation) { + if (axisalingedbb.voxelShapeIntersect(boundingBox.minX + offX, boundingBox.minY + offY, boundingBox.minZ + offZ, + boundingBox.maxX + offX, boundingBox.maxY + offY, boundingBox.maxZ + offZ)) { + return true; + } + } + + return false; + } + // Tuinity end - optimise multi-aabb shapes } diff --git a/src/main/java/net/minecraft/server/VoxelShapeSpliterator.java b/src/main/java/net/minecraft/server/VoxelShapeSpliterator.java index e841611bb7c36dffec44bb9e74a0a9657a113263..259605daabb18aedb15d56c78e6553ae2d22e13f 100644 --- a/src/main/java/net/minecraft/server/VoxelShapeSpliterator.java +++ b/src/main/java/net/minecraft/server/VoxelShapeSpliterator.java @@ -91,7 +91,7 @@ public class VoxelShapeSpliterator extends AbstractSpliterator { VoxelShape voxelshape = iblockdata.b((IBlockAccess) this.g, this.e, this.c); if (voxelshape == VoxelShapes.b()) { - if (!this.b.a((double) i, (double) j, (double) k, (double) i + 1.0D, (double) j + 1.0D, (double) k + 1.0D)) { + if (!this.b.voxelShapeIntersect((double) i, (double) j, (double) k, (double) i + 1.0D, (double) j + 1.0D, (double) k + 1.0D)) { // Tuinity - keep vanilla behavior for voxelshape intersection - See comment in AxisAlignedBB continue; } diff --git a/src/main/java/net/minecraft/server/VoxelShapes.java b/src/main/java/net/minecraft/server/VoxelShapes.java index e21c747b6c39155c44bf30860681d67b0b29fb12..636bbbc42466cb54c300352f400464fe64cc2e79 100644 --- a/src/main/java/net/minecraft/server/VoxelShapes.java +++ b/src/main/java/net/minecraft/server/VoxelShapes.java @@ -17,18 +17,101 @@ public final class VoxelShapes { voxelshapebitset.a(0, 0, 0, true, true); return new VoxelShapeCube(voxelshapebitset); - }); + }); public static final VoxelShape getFullUnoptimisedCube() { return VoxelShapes.b; } // Tuinity - OBFHELPER public static final VoxelShape a = create(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); - private static final VoxelShape c = new VoxelShapeArray(new VoxelShapeBitSet(0, 0, 0), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D})); + private static final VoxelShape c = new VoxelShapeArray(new VoxelShapeBitSet(0, 0, 0), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D})); public static final VoxelShape getEmptyShape() { return VoxelShapes.c; } // Tuinity - OBFHELPER + + // Tuinity start - optimise voxelshapes + public static boolean isEmpty(VoxelShape voxelshape) { + // helper function for determining empty shapes fast + return voxelshape == getEmptyShape() || voxelshape.isEmpty(); + } + // Tuinity end - optimise voxelshapes public static final VoxelShape empty() {return a();} // Paper - OBFHELPER public static VoxelShape a() { return VoxelShapes.c; } + static final com.tuinity.tuinity.voxel.AABBVoxelShape optimisedFullCube = new com.tuinity.tuinity.voxel.AABBVoxelShape(new AxisAlignedBB(0, 0, 0, 1.0, 1.0, 1.0)); // Tuinity - optimise voxelshape + + // Tuinity start - optimise voxelshapes + public static boolean addBoxesToIfIntersects(VoxelShape shape, AxisAlignedBB aabb, java.util.List list) { + if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { + com.tuinity.tuinity.voxel.AABBVoxelShape shapeCasted = (com.tuinity.tuinity.voxel.AABBVoxelShape)shape; + if (!shapeCasted.aabb.isEmpty() && shapeCasted.aabb.voxelShapeIntersect(aabb)) { + list.add(shapeCasted.aabb); + return true; + } + return false; + } else if (shape instanceof VoxelShapeArray) { + VoxelShapeArray shapeCasted = (VoxelShapeArray)shape; + // this can be optimised by checking an "overall shape" first, but not needed + + double offX = shapeCasted.offsetX; + double offY = shapeCasted.offsetY; + double offZ = shapeCasted.offsetZ; + + boolean ret = false; + + for (AxisAlignedBB boundingBox : shapeCasted.boundingBoxesRepresentation) { + double minX, minY, minZ, maxX, maxY, maxZ; + if (aabb.voxelShapeIntersect(minX = boundingBox.minX + offX, minY = boundingBox.minY + offY, minZ = boundingBox.minZ + offZ, + maxX = boundingBox.maxX + offX, maxY = boundingBox.maxY + offY, maxZ = boundingBox.maxZ + offZ)) { + AxisAlignedBB box = new AxisAlignedBB(minX, minY, minZ, maxX, maxY, maxZ, false); + if (!box.isEmpty()) { + list.add(box); + ret = true; + } + } + } + + return ret; + } else { + boolean ret = false; + + java.util.List boxes = shape.getBoundingBoxesRepresentation(); + for (int i = 0, len = boxes.size(); i < len; ++i) { + AxisAlignedBB box = boxes.get(i); + if (!box.isEmpty() && box.voxelShapeIntersect(aabb)) { + list.add(box); + ret = true; + } + } + + return ret; + } + } + + public static void addBoxesTo(VoxelShape shape, java.util.List list) { + if (shape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { + com.tuinity.tuinity.voxel.AABBVoxelShape shapeCasted = (com.tuinity.tuinity.voxel.AABBVoxelShape)shape; + if (!shapeCasted.isEmpty()) { + list.add(shapeCasted.aabb); + } + } else if (shape instanceof VoxelShapeArray) { + VoxelShapeArray shapeCasted = (VoxelShapeArray)shape; + + for (AxisAlignedBB boundingBox : shapeCasted.boundingBoxesRepresentation) { + if (!boundingBox.isEmpty()) { + list.add(boundingBox.offset(shapeCasted.offsetX, shapeCasted.offsetY, shapeCasted.offsetZ)); + } + } + } else { + java.util.List boxes = shape.getBoundingBoxesRepresentation(); + for (int i = 0, len = boxes.size(); i < len; ++i) { + AxisAlignedBB box = boxes.get(i); + if (!box.isEmpty()) { + list.add(box); + } + } + } + } + // Tuinity end - optimise voxelshapes + public static final VoxelShape fullCube() {return b();} // Paper - OBFHELPER public static VoxelShape b() { - return VoxelShapes.b; + return VoxelShapes.optimisedFullCube; // Tuinity - optimise voxelshape } public static VoxelShape create(double d0, double d1, double d2, double d3, double d4, double d5) { @@ -67,7 +150,7 @@ public final class VoxelShapes { return new VoxelShapeCube(voxelshapebitset); } } else { - return new VoxelShapeArray(VoxelShapes.b.a, new double[]{axisalignedbb.minX, axisalignedbb.maxX}, new double[]{axisalignedbb.minY, axisalignedbb.maxY}, new double[]{axisalignedbb.minZ, axisalignedbb.maxZ}); + return new com.tuinity.tuinity.voxel.AABBVoxelShape(axisalignedbb); // Tuinity - optimise VoxelShapes for single AABB shapes } } @@ -132,6 +215,20 @@ public final class VoxelShapes { public static final boolean applyOperation(VoxelShape voxelshape, VoxelShape voxelshape1, OperatorBoolean operatorboolean) { return VoxelShapes.c(voxelshape, voxelshape1, operatorboolean); } // Paper - OBFHELPER public static boolean c(VoxelShape voxelshape, VoxelShape voxelshape1, OperatorBoolean operatorboolean) { + // Tuinity start - optimise voxelshape + if (operatorboolean == OperatorBoolean.AND) { + if (voxelshape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && voxelshape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) { + return ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb.voxelShapeIntersect(((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb); + } else if (voxelshape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && voxelshape1 instanceof VoxelShapeArray) { + return ((VoxelShapeArray)voxelshape1).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb); + } else if (voxelshape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape && voxelshape instanceof VoxelShapeArray) { + return ((VoxelShapeArray)voxelshape).intersects(((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb); + } + } + return abstract_c(voxelshape, voxelshape1, operatorboolean); + } + public static boolean abstract_c(VoxelShape voxelshape, VoxelShape voxelshape1, OperatorBoolean operatorboolean) { + // Tuinity end - optimise voxelshape if (operatorboolean.apply(false, false)) { throw (IllegalArgumentException) SystemUtils.c((Throwable) (new IllegalArgumentException())); } else if (voxelshape == voxelshape1) { @@ -314,8 +411,52 @@ public final class VoxelShapes { } } + public static boolean combinationOccludes(VoxelShape voxelshape, VoxelShape voxelshape1) { return b(voxelshape, voxelshape1); } // Tuinity - OBFHELPER public static boolean b(VoxelShape voxelshape, VoxelShape voxelshape1) { - return voxelshape != b() && voxelshape1 != b() ? (voxelshape.isEmpty() && voxelshape1.isEmpty() ? false : !c(b(), b(voxelshape, voxelshape1, OperatorBoolean.OR), OperatorBoolean.ONLY_FIRST)) : true; + if (voxelshape == getFullUnoptimisedCube() || voxelshape == optimisedFullCube + || voxelshape1 == getFullUnoptimisedCube() || voxelshape1 == optimisedFullCube) { + return true; + } + boolean v1Empty = voxelshape == getEmptyShape(); + boolean v2Empty = voxelshape1 == getEmptyShape(); + if (v1Empty && v2Empty) { + return false; + } + if ((voxelshape instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v1Empty) && (voxelshape1 instanceof com.tuinity.tuinity.voxel.AABBVoxelShape || v2Empty)) { + if (!v1Empty && !v2Empty && (voxelshape != voxelshape1)) { + AxisAlignedBB boundingBox1 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb; + AxisAlignedBB boundingBox2 = ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb; + // can call it here in some cases + + // check overall bounding box + double minY = Math.min(boundingBox1.minY, boundingBox2.minY); + double maxY = Math.max(boundingBox1.maxY, boundingBox2.maxY); + if (minY > MCUtil.COLLISION_EPSILON || maxY < (1 - MCUtil.COLLISION_EPSILON)) { + return false; + } + double minX = Math.min(boundingBox1.minX, boundingBox2.minX); + double maxX = Math.max(boundingBox1.maxX, boundingBox2.maxX); + if (minX > MCUtil.COLLISION_EPSILON || maxX < (1 - MCUtil.COLLISION_EPSILON)) { + return false; + } + double minZ = Math.min(boundingBox1.minZ, boundingBox2.minZ); + double maxZ = Math.max(boundingBox1.maxZ, boundingBox2.maxZ); + if (minZ > MCUtil.COLLISION_EPSILON || maxZ < (1 - MCUtil.COLLISION_EPSILON)) { + return false; + } + // fall through to full merge check + } else { + AxisAlignedBB boundingBox = v1Empty ? ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape1).aabb : ((com.tuinity.tuinity.voxel.AABBVoxelShape)voxelshape).aabb; + // check if the bounding box encloses the full cube + return (boundingBox.minY <= MCUtil.COLLISION_EPSILON && boundingBox.maxY >= (1 - MCUtil.COLLISION_EPSILON)) && + (boundingBox.minX <= MCUtil.COLLISION_EPSILON && boundingBox.maxX >= (1 - MCUtil.COLLISION_EPSILON)) && + (boundingBox.minZ <= MCUtil.COLLISION_EPSILON && boundingBox.maxZ >= (1 - MCUtil.COLLISION_EPSILON)); + } + } + return b_rare(voxelshape, voxelshape1); + } + public static boolean b_rare(VoxelShape voxelshape, VoxelShape voxelshape1) { + return (voxelshape != b() || voxelshape != getFullUnoptimisedCube()) && (voxelshape1 != b() || voxelshape1 != getFullUnoptimisedCube()) ? ((voxelshape == VoxelShapes.getEmptyShape() || voxelshape.isEmpty()) && (voxelshape1 == VoxelShapes.getEmptyShape() || voxelshape1.isEmpty()) ? false : !c(b(), b(voxelshape, voxelshape1, OperatorBoolean.OR), OperatorBoolean.ONLY_FIRST)) : true; // Tuinity - optimise call by checking against more constant shapes } @VisibleForTesting diff --git a/src/main/java/net/minecraft/server/WeightedList.java b/src/main/java/net/minecraft/server/WeightedList.java index 5d9d58411f2fad9d5da703f964d269b4a7c2b205..f0fdfd6891e59891e7370a2d682b65c647b28e9e 100644 --- a/src/main/java/net/minecraft/server/WeightedList.java +++ b/src/main/java/net/minecraft/server/WeightedList.java @@ -14,7 +14,7 @@ import java.util.stream.Stream; public class WeightedList { - protected final List> list; // Paper - decompile conflict + protected final List> list; public final List> getList() { return this.list; } // Paper - decompile conflict // Tuinity - OBFHELPER private final Random b; private final boolean isUnsafe; // Paper @@ -74,7 +74,7 @@ public class WeightedList { public static class a { - private final T a; + private final T a; public final T getValue() { return this.a; } // Tuinity - OBFHELPER private final int b; private double c; diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java index 02303f00e243748b9d1c4a37719fcf5c8d271ed9..28ee325fcc8b50397768363403823f2e3391d8c8 100644 --- a/src/main/java/net/minecraft/server/World.java +++ b/src/main/java/net/minecraft/server/World.java @@ -94,6 +94,8 @@ public abstract class World implements GeneratorAccess, AutoCloseable { public final com.destroystokyo.paper.PaperWorldConfig paperConfig; // Paper public final ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray + public final com.tuinity.tuinity.config.TuinityConfig.WorldConfig tuinityConfig; // Tuinity - Server Config + public final co.aikar.timings.WorldTimingsHandler timings; // Paper public static BlockPosition lastPhysicsProblem; // Spigot private org.spigotmc.TickLimiter entityLimiter; @@ -121,10 +123,39 @@ public abstract class World implements GeneratorAccess, AutoCloseable { return typeKey; } + // Tuinity start - optimise checkDespawn + public final List getNearbyPlayers(Entity source, double maxRange, Predicate predicate) { + Chunk chunk = source.getCurrentChunk(); + if (chunk == null || maxRange < 0.0 || maxRange > 31.0*16.0) { + return this.getNearbyPlayersSlow(source, maxRange, predicate); + } + + List ret = new java.util.ArrayList<>(); + chunk.getNearestPlayers(source, predicate, maxRange, ret); + return ret; + } + + private List getNearbyPlayersSlow(Entity source, double maxRange, Predicate predicate) { + List ret = new java.util.ArrayList<>(); + double maxRangeSquared = maxRange * maxRange; + + for (EntityHuman player : this.getPlayers()) { + if ((maxRange < 0.0 || player.getDistanceSquared(source.locX(), source.locY(), source.locZ()) < maxRangeSquared)) { + if (predicate == null || predicate.test(player)) { + ret.add((EntityPlayer)player); + } + } + } + + return ret; + } + // Tuinity end - optimise checkDespawn + protected World(WorldDataMutable worlddatamutable, ResourceKey resourcekey, final DimensionManager dimensionmanager, Supplier supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((WorldDataServer) worlddatamutable).getName()); // Spigot this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(((WorldDataServer) worlddatamutable).getName(), this.spigotConfig); // Paper this.chunkPacketBlockController = this.paperConfig.antiXray ? new ChunkPacketBlockControllerAntiXray(this, executor) : ChunkPacketBlockController.NO_OPERATION_INSTANCE; // Paper - Anti-Xray + this.tuinityConfig = new com.tuinity.tuinity.config.TuinityConfig.WorldConfig(((WorldDataServer)worlddatamutable).getName()); // Tuinity - Server Config this.generator = gen; this.world = new CraftWorld((WorldServer) this, gen, env); this.ticksPerAnimalSpawns = this.getServer().getTicksPerAnimalSpawns(); // CraftBukkit @@ -286,6 +317,15 @@ public abstract class World implements GeneratorAccess, AutoCloseable { @Override public final Chunk getChunkAt(int i, int j) { // Paper - final to help inline + // Tuinity start - make sure loaded chunks get the inlined variant of this function + ChunkProviderServer cps = ((WorldServer)this).chunkProvider; + if (cps.serverThread == Thread.currentThread()) { + Chunk ifLoaded = cps.getChunkAtIfLoadedMainThread(i, j); + if (ifLoaded != null) { + return ifLoaded; + } + } + // Tuinity end - make sure loaded chunks get the inlined variant of this function return (Chunk) this.getChunkAt(i, j, ChunkStatus.FULL, true); // Paper - avoid a method jump } @@ -360,6 +400,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable { @Override public boolean a(BlockPosition blockposition, IBlockData iblockdata, int i, int j) { + org.spigotmc.AsyncCatcher.catchOp("set type call"); // Tuinity // CraftBukkit start - tree generation if (this.captureTreeGeneration) { // Paper start @@ -461,6 +502,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable { // CraftBukkit start - Split off from above in order to directly send client and physic updates public void notifyAndUpdatePhysics(BlockPosition blockposition, Chunk chunk, IBlockData oldBlock, IBlockData newBlock, IBlockData actualBlock, int i, int j) { + com.tuinity.tuinity.util.TickThread.softEnsureTickThread("Async notify and update"); // Tuinity IBlockData iblockdata = newBlock; IBlockData iblockdata1 = oldBlock; IBlockData iblockdata2 = actualBlock; @@ -896,6 +938,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable { return; // Paper end } + MinecraftServer.getServer().executeMidTickTasks(); // Tuinity - execute chunk tasks mid tick } // Paper start - Prevent armor stands from doing entity lookups @Override @@ -1079,10 +1122,44 @@ public abstract class World implements GeneratorAccess, AutoCloseable { return this.getChunkAt(i, j, ChunkStatus.FULL, false); } + // Tuinity start - optimise hard collision handling + @Override + public List getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + return this.getHardCollidingEntities(entity, axisalignedbb, predicate, Lists.newArrayList()); + } + + public List getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate, List list) { + // copied from below + int i = MathHelper.floor((axisalignedbb.minX - 2.0D) / 16.0D); + int j = MathHelper.floor((axisalignedbb.maxX + 2.0D) / 16.0D); + int k = MathHelper.floor((axisalignedbb.minZ - 2.0D) / 16.0D); + int l = MathHelper.floor((axisalignedbb.maxZ + 2.0D) / 16.0D); + + ChunkProviderServer chunkProvider = ((WorldServer)this).getChunkProvider(); + + for (int i1 = i; i1 <= j; ++i1) { + for (int j1 = k; j1 <= l; ++j1) { + Chunk chunk = chunkProvider.getChunkAtIfLoadedMainThread(i1, j1); + + if (chunk != null) { + chunk.getHardCollidingEntities(entity, axisalignedbb, list, predicate); + } + } + } + + return list; + } + // Tuinity end - optimise hard collision handling + @Override public List getEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate predicate) { - this.getMethodProfiler().c("getEntities"); + // Tuinity start - add list parameter List list = Lists.newArrayList(); + return this.getEntities(entity, axisalignedbb, predicate, list); + } + public List getEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate predicate, List list) { + // Tuinity end - add list parameter + this.getMethodProfiler().c("getEntities"); int i = MathHelper.floor((axisalignedbb.minX - 2.0D) / 16.0D); int j = MathHelper.floor((axisalignedbb.maxX + 2.0D) / 16.0D); int k = MathHelper.floor((axisalignedbb.minZ - 2.0D) / 16.0D); @@ -1138,7 +1215,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable { Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper if (chunk != null) { - chunk.a(oclass, axisalignedbb, list, predicate); + chunk.getEntitiesClass(oclass, null, axisalignedbb, (Predicate)predicate, (List)list); // Tuinity - optimise lookup by entity class } } } @@ -1161,7 +1238,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable { Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper if (chunk != null) { - chunk.a(oclass, axisalignedbb, list, predicate); + chunk.getEntitiesClass(oclass, null, axisalignedbb, (Predicate)predicate, (List)list); // Tuinity - optimise lookup by entity class } } } @@ -1169,6 +1246,106 @@ public abstract class World implements GeneratorAccess, AutoCloseable { return list; } + // Tuinity start + @Override + public T b(Class oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { + return this.getClosestEntity(oclass, pathfindertargetcondition, entityliving, d0, d1, d2, axisalignedbb); + } + + @Override + public T a(Class oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { + return this.getClosestEntity(oclass, pathfindertargetcondition, entityliving, d0, d1, d2, axisalignedbb); + } + + public final T getClosestEntity(Class clazz, + PathfinderTargetCondition condition, + @Nullable EntityLiving source, + double x, double y, double z, + AxisAlignedBB boundingBox) { + org.bukkit.craftbukkit.util.UnsafeList entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList(); + try { + int lowerX = MCUtil.fastFloor((boundingBox.minX - 2.0D)) >> 4; + int upperX = MCUtil.fastFloor((boundingBox.maxX + 2.0D)) >> 4; + int lowerZ = MCUtil.fastFloor((boundingBox.minZ - 2.0D)) >> 4; + int upperZ = MCUtil.fastFloor((boundingBox.maxZ + 2.0D)) >> 4; + + org.bukkit.craftbukkit.util.UnsafeList chunks = com.tuinity.tuinity.util.CachedLists.getTempGetChunksList(); + try { + T closest = null; + double closestDistance = Double.MAX_VALUE; + ChunkProviderServer chunkProvider = ((WorldServer)this).getChunkProvider(); + + int centerX = (lowerX + upperX) >> 1; + int centerZ = (lowerZ + upperZ) >> 1; + // Copied from MCUtil.getSpiralOutChunks + Chunk temp; + if ((temp = chunkProvider.getChunkAtIfLoadedImmediately(centerX, centerZ)) != null && temp.hasEntitiesMaybe(clazz)) { + chunks.add(temp); + } + int radius = Math.max((upperX - lowerX + 1) >> 1, (upperZ - lowerZ + 1) >> 1); + for (int r = 1; r <= radius; r++) { + int ox = -r; + int oz = r; + + // Iterates the edge of half of the box; then negates for other half. + while (ox <= r && oz > -r) { + { + int cx = centerX + ox; + int cz = centerZ + oz; + if (cx >= lowerX && cx <= upperX && cz >= lowerZ && cz <= upperZ && + (temp = chunkProvider.getChunkAtIfLoadedImmediately(cx, cz)) != null && + temp.hasEntitiesMaybe(clazz)) { + chunks.add(temp); + } + } + { + int cx = centerX - ox; + int cz = centerZ - oz; + if (cx >= lowerX && cx <= upperX && cz >= lowerZ && cz <= upperZ && + (temp = chunkProvider.getChunkAtIfLoadedImmediately(cx, cz)) != null && + temp.hasEntitiesMaybe(clazz)) { + chunks.add(temp); + } + } + + if (ox < r) { + ox++; + } else { + oz--; + } + } + } + + Object[] chunkData = chunks.getRawDataArray(); + for (int cindex = 0, clen = chunks.size(); cindex < clen; ++cindex) { + final Chunk chunk = (Chunk)chunkData[cindex]; + + chunk.getEntitiesClass(clazz, source, boundingBox, null, entities); + + Object[] entityData = entities.getRawDataArray(); + for (int eindex = 0, entities_len = entities.size(); eindex < entities_len; ++eindex) { + T entity = (T)entityData[eindex]; + double distance = entity.getDistanceSquared(x, y, z); + // check distance first, as it's the least expensive + if (distance < closestDistance && condition.test(source, entity)) { + closest = entity; + closestDistance = distance; + } + } + + entities.setSize(0); + } + + return closest; + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempGetChunksList(chunks); + } + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities); + } + } + // Tuinity end + @Nullable public abstract Entity getEntity(int i); diff --git a/src/main/java/net/minecraft/server/WorldBorder.java b/src/main/java/net/minecraft/server/WorldBorder.java index f011869880fedae4b69e505491e8bdbc5f51dfba..0d10d317cd0b60fc0866ae505c7fd71fa886c48b 100644 --- a/src/main/java/net/minecraft/server/WorldBorder.java +++ b/src/main/java/net/minecraft/server/WorldBorder.java @@ -47,11 +47,59 @@ public class WorldBorder { return axisalignedbb.maxX > this.e() && axisalignedbb.minX < this.g() && axisalignedbb.maxZ > this.f() && axisalignedbb.minZ < this.h(); } + // Tuinity start - optimise collisions + // determines whether we are almost colliding with the world border + // for clear collisions, this rets false + public final boolean isAlmostCollidingOnBorder(AxisAlignedBB boundingBox) { + return this.isAlmostCollidingOnBorder(boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public final boolean isAlmostCollidingOnBorder(double boxMinX, double boxMaxX, double boxMinZ, double boxMaxZ) { + double borderMinX = this.getMinX(); + double borderMaxX = this.getMaxX(); + + double borderMinZ = this.getMinZ(); + double borderMaxZ = this.getMaxZ(); + + return + // Not intersecting if we're smaller + !AxisAlignedBB.voxelShapeIntersect( + boxMinX + MCUtil.COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ + MCUtil.COLLISION_EPSILON, + boxMaxX - MCUtil.COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ - MCUtil.COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ) + && + + // Are intersecting if we're larger + AxisAlignedBB.voxelShapeIntersect( + boxMinX - MCUtil.COLLISION_EPSILON, Double.NEGATIVE_INFINITY, boxMinZ - MCUtil.COLLISION_EPSILON, + boxMaxX + MCUtil.COLLISION_EPSILON, Double.POSITIVE_INFINITY, boxMaxZ + MCUtil.COLLISION_EPSILON, + borderMinX, Double.NEGATIVE_INFINITY, borderMinZ, borderMaxX, Double.POSITIVE_INFINITY, borderMaxZ + ) + ; + } + + public final boolean isCollidingWithBorderEdge(AxisAlignedBB boundingBox) { + return this.isCollidingWithBorderEdge(boundingBox.minX, boundingBox.maxX, boundingBox.minZ, boundingBox.maxZ); + } + + public final boolean isCollidingWithBorderEdge(double boxMinX, double boxMaxX, double boxMinZ, double boxMaxZ) { + double borderMinX = this.getMinX() + MCUtil.COLLISION_EPSILON; + double borderMaxX = this.getMaxX() - MCUtil.COLLISION_EPSILON; + + double borderMinZ = this.getMinZ() + MCUtil.COLLISION_EPSILON; + double borderMaxZ = this.getMaxZ() - MCUtil.COLLISION_EPSILON; + + return boxMinX < borderMinX || boxMaxX > borderMaxX || boxMinZ < borderMinZ || boxMaxZ > borderMaxZ; + } + // Tuinity end - optimise collisions + public double a(Entity entity) { return this.b(entity.locX(), entity.locZ()); } public final VoxelShape asVoxelShape(){ return c();} // Paper - OBFHELPER + public final VoxelShape getCollisionShape() { return this.c(); } // Tuinity - OBFHELPER public VoxelShape c() { return this.j.m(); } @@ -67,18 +115,22 @@ public class WorldBorder { return Math.min(d6, d3); } + public final double getMinX() { return this.e(); } // Tuinity - OBFHELPER public double e() { return this.j.a(); } + public final double getMinZ() { return this.f(); } // Tuinity - OBFHELPER public double f() { return this.j.c(); } + public final double getMaxX() { return this.g(); } // Tuinity - OBFHELPER public double g() { return this.j.b(); } + public final double getMaxZ() { return this.h(); } // Tuinity - OBFHELPER public double h() { return this.j.d(); } diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java index fbe7f43f6c1010e7a34114f8afb0e64934744335..6ff5ef6b710652f1c4fe6461ff230ee78988f623 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -55,12 +55,13 @@ import org.bukkit.event.server.MapInitializeEvent; import org.bukkit.event.weather.LightningStrikeEvent; import org.bukkit.event.world.TimeSkipEvent; // CraftBukkit end +import it.unimi.dsi.fastutil.ints.IntArrayList; // Tuinity public class WorldServer extends World implements GeneratorAccessSeed { public static final BlockPosition a = new BlockPosition(100, 50, 0); private static final Logger LOGGER = LogManager.getLogger(); - public final Int2ObjectMap entitiesById = new Int2ObjectLinkedOpenHashMap(); + public final Int2ObjectMap entitiesById = new Int2ObjectLinkedOpenHashMap(); final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet entitiesForIteration = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(2048, 0.5f, 2048, 0.2, true); // Tuinity - make removing entities while ticking safe private final Map entitiesByUUID = Maps.newHashMap(); private final Queue entitiesToAdd = Queues.newArrayDeque(); public final List players = Lists.newArrayList(); // Paper - private -> public @@ -84,7 +85,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { private final PortalTravelAgent portalTravelAgent; private final TickListServer nextTickListBlock; private final TickListServer nextTickListFluid; - private final Set navigators; + private final Set navigators; final com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigatorsForIteration = new com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet<>(2048, 0.5f, 2048, 0.2, true); // Tuinity - make removing entities while ticking safe protected final PersistentRaid persistentRaid; private final ObjectLinkedOpenHashSet L; private boolean ticking; @@ -205,6 +206,111 @@ public class WorldServer extends World implements GeneratorAccessSeed { } // Paper end - rewrite ticklistserver + // Tuinity start + public final boolean areChunksLoadedForMove(AxisAlignedBB axisalignedbb) { + // copied code from collision methods, so that we can guarantee that they wont load chunks (we don't override + // ICollisionAccess methods for VoxelShapes) + // be more strict too, add a block (dumb plugins in move events?) + int minBlockX = MathHelper.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int maxBlockX = MathHelper.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + + int minBlockZ = MathHelper.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ChunkProviderServer chunkProvider = this.getChunkProvider(); + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { + return false; + } + } + } + + return true; + } + + public final void loadChunksForMoveAsync(AxisAlignedBB axisalignedbb, double toX, double toZ, + java.util.function.Consumer> onLoad) { + if (Thread.currentThread() != this.serverThread) { + this.getChunkProvider().serverThreadQueue.execute(() -> { + this.loadChunksForMoveAsync(axisalignedbb, toX, toZ, onLoad); + }); + return; + } + List ret = new java.util.ArrayList<>(); + IntArrayList ticketLevels = new IntArrayList(); + + int minBlockX = MathHelper.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int maxBlockX = MathHelper.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + + int minBlockZ = MathHelper.floor(axisalignedbb.minZ - 1.0E-7D) - 3; + int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ChunkProviderServer chunkProvider = this.getChunkProvider(); + + int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); + int[] loadedChunks = new int[1]; + + Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++); + + java.util.function.Consumer consumer = (IChunkAccess chunk) -> { + if (chunk != null) { + int ticketLevel = Math.max(33, chunkProvider.playerChunkMap.getUpdatingChunk(chunk.getPos().pair()).getTicketLevel()); + ret.add(chunk); + ticketLevels.add(ticketLevel); + chunkProvider.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunk.getPos(), ticketLevel, holderIdentifier); + } + if (++loadedChunks[0] == requiredChunks) { + try { + onLoad.accept(java.util.Collections.unmodifiableList(ret)); + } finally { + for (int i = 0, len = ret.size(); i < len; ++i) { + ChunkCoordIntPair chunkPos = ret.get(i).getPos(); + int ticketLevel = ticketLevels.getInt(i); + + chunkProvider.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos); + chunkProvider.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, holderIdentifier); + } + } + } + }; + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + chunkProvider.getChunkAtAsynchronously(cx, cz, ChunkStatus.FULL, true, false, consumer); + } + } + } + // Tuinity end + + // Tuinity start - execute chunk tasks mid tick + long lastMidTickExecuteFailure; + // Tuinity end - execute chunk tasks mid tick + + // Tuinity start - optimise checkDespawn + final List playersAffectingSpawning = new java.util.ArrayList<>(); + // Tuinity end - optimise checkDespawn + + // Tuinity start - rewrite light engine + /** + * Cannot be modified during light updates. + */ + public com.tuinity.tuinity.chunk.light.VariableBlockLightHandler customBlockLightHandlers; + // Tuinity end - rewrite light engine + // Add env and gen to constructor, WorldData -> WorldDataServer public WorldServer(MinecraftServer minecraftserver, Executor executor, Convertable.ConversionSession convertable_conversionsession, IWorldDataServer iworlddataserver, ResourceKey resourcekey, DimensionManager dimensionmanager, WorldLoadListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) { super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getMethodProfiler, false, flag, i, gen, env, executor); // Paper pass executor @@ -265,6 +371,243 @@ public class WorldServer extends World implements GeneratorAccessSeed { this.asyncChunkTaskManager = new com.destroystokyo.paper.io.chunk.ChunkTaskManager(this); // Paper } + // Tuinity start - optimise collision + public boolean collidesWithAnyBlockOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, boolean loadChunks, + boolean collidesWithUnloaded) { + return this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, loadChunks, collidesWithUnloaded, true, null); + } + + public boolean collidesWithAnyBlockOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, + boolean loadChunks, boolean collidesWithUnloaded, + java.util.function.BiPredicate predicate) { + return this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, null, loadChunks, collidesWithUnloaded, true, predicate); + } + + public final boolean hardCollidesWithAnyEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate predicate) { + if (axisalignedbb.isEmpty()) { + return false; + } + axisalignedbb = axisalignedbb.grow(MCUtil.COLLISION_EPSILON, MCUtil.COLLISION_EPSILON, MCUtil.COLLISION_EPSILON); + List entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList(); + try { + if (entity != null && entity.hardCollides()) { + this.getEntities(entity, axisalignedbb, predicate, entities); + } else { + this.getHardCollidingEntities(entity, axisalignedbb, predicate, entities); + } + + for (int i = 0, len = entities.size(); i < len; ++i) { + Entity otherEntity = entities.get(i); + + if ((entity == null || otherEntity.collisionBoxIsHard()) || entity.hardCollidesWith(otherEntity)) { + return true; + } + } + + return false; + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities); + } + } + + public final boolean hasAnyCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb) { + return this.hasAnyCollisions(entity, axisalignedbb, true); + } + + public final boolean hasAnyCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, boolean loadChunks) { + return this.collidesWithAnyBlockOrWorldBorder(entity, axisalignedbb, loadChunks, true) + || this.hardCollidesWithAnyEntities(entity, axisalignedbb, null); + } + + // returns whether any collisions were detected + public boolean getCollisionsForBlocksOrWorldBorder(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List list, + boolean loadChunks, boolean collidesWithUnloaded, boolean checkOnly, + java.util.function.BiPredicate predicate) { + boolean ret = false; + + if (entity != null) { + if (this.getWorldBorder().isAlmostCollidingOnBorder(axisalignedbb)) { + if (checkOnly) { + return true; + } else { + VoxelShapes.addBoxesTo(this.getWorldBorder().getCollisionShape(), list); + ret = true; + } + } + } + + int minBlockX = MathHelper.floor(axisalignedbb.minX - MCUtil.COLLISION_EPSILON) - 1; + int maxBlockX = MathHelper.floor(axisalignedbb.maxX + MCUtil.COLLISION_EPSILON) + 1; + + int minBlockY = MathHelper.floor(axisalignedbb.minY - MCUtil.COLLISION_EPSILON) - 1; + int maxBlockY = MathHelper.floor(axisalignedbb.maxY + MCUtil.COLLISION_EPSILON) + 1; + + int minBlockZ = MathHelper.floor(axisalignedbb.minZ - MCUtil.COLLISION_EPSILON) - 1; + int maxBlockZ = MathHelper.floor(axisalignedbb.maxZ + MCUtil.COLLISION_EPSILON) + 1; + + + BlockPosition.MutableBlockPosition mutablePos = new BlockPosition.MutableBlockPosition(); + VoxelShapeCollision collisionShape = null; + + // special cases: + if (minBlockY > 255 || maxBlockY < 0) { + // no point in checking + return ret; + } + + int minYIterate = Math.max(0, minBlockY); + int maxYIterate = Math.min(255, maxBlockY); + + int minChunkX = minBlockX >> 4; + int maxChunkX = maxBlockX >> 4; + + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + + ChunkProviderServer chunkProvider = (ChunkProviderServer)this.chunkProvider; + // TODO special case single chunk? + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + int minZ = currChunkZ == minChunkZ ? minBlockZ & 15 : 0; // coordinate in chunk + int maxZ = currChunkZ == maxChunkZ ? maxBlockZ & 15 : 15; // coordinate in chunk + + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + int minX = currChunkX == minChunkX ? minBlockX & 15 : 0; // coordinate in chunk + int maxX = currChunkX == maxChunkX ? maxBlockX & 15 : 15; // coordinate in chunk + + int chunkXGlobalPos = currChunkX << 4; + int chunkZGlobalPos = currChunkZ << 4; + Chunk chunk = loadChunks ? chunkProvider.getChunkAt(currChunkX, currChunkZ, true) : chunkProvider.getChunkAtIfLoadedImmediately(currChunkX, currChunkZ); + + if (chunk == null) { + if (collidesWithUnloaded) { + if (checkOnly) { + return true; + } else { + list.add(AxisAlignedBB.getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + } + } + continue; + } + + ChunkSection[] sections = chunk.getSections(); + + // bound y + + for (int currY = minYIterate; currY <= maxYIterate; ++currY) { + ChunkSection section = sections[currY >>> 4]; + if (section == null || section.isFullOfAir()) { + // empty + // skip to next section + currY = (currY & ~(15)) + 15; // increment by 15: iterator loop increments by the extra one + continue; + } + + DataPaletteBlock blocks = section.blockIds; + + for (int currZ = minZ; currZ <= maxZ; ++currZ) { + for (int currX = minX; currX <= maxX; ++currX) { + int localBlockIndex = (currX) | (currZ << 4) | ((currY & 15) << 8); + int blockX = currX | chunkXGlobalPos; + int blockY = currY; + int blockZ = currZ | chunkZGlobalPos; + + int edgeCount = ((blockX == minBlockX || blockX == maxBlockX) ? 1 : 0) + + ((blockY == minBlockY || blockY == maxBlockY) ? 1 : 0) + + ((blockZ == minBlockZ || blockZ == maxBlockZ) ? 1 : 0); + if (edgeCount == 3) { + continue; + } + + IBlockData blockData = blocks.rawGet(localBlockIndex); + + if ((edgeCount != 1 || blockData.shapeExceedsCube()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON)) { + mutablePos.setValues(blockX, blockY, blockZ); + if (collisionShape == null) { + collisionShape = entity == null ? VoxelShapeCollision.a() : VoxelShapeCollision.a(entity); + } + VoxelShape voxelshape2 = blockData.getCollisionShape(this, mutablePos, collisionShape); + if (voxelshape2 != VoxelShapes.getEmptyShape()) { + VoxelShape voxelshape3 = voxelshape2.offset((double)blockX, (double)blockY, (double)blockZ); + + if (predicate != null && !predicate.test(blockData, mutablePos)) { + continue; + } + + if (checkOnly) { + if (voxelshape3.intersects(axisalignedbb)) { + return true; + } + } else { + ret |= VoxelShapes.addBoxesToIfIntersects(voxelshape3, axisalignedbb, list); + } + } + } + } + } + } + } + } + + return ret; + } + + public final void getEntityHardCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, @Nullable Predicate predicate, List list) { + if (axisalignedbb.isEmpty()) { + return; + } + axisalignedbb = axisalignedbb.grow(MCUtil.COLLISION_EPSILON, MCUtil.COLLISION_EPSILON, MCUtil.COLLISION_EPSILON); + List entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList(); + try { + if (entity != null && entity.hardCollides()) { + this.getEntities(entity, axisalignedbb, predicate, entities); + } else { + this.getHardCollidingEntities(entity, axisalignedbb, predicate, entities); + } + + for (int i = 0, len = entities.size(); i < len; ++i) { + Entity otherEntity = entities.get(i); + + if ((entity == null || otherEntity.collisionBoxIsHard()) || entity.hardCollidesWith(otherEntity)) { + if (!otherEntity.getBoundingBox().isEmpty()) { + list.add(otherEntity.getBoundingBox()); + } + } + } + } finally { + com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities); + } + } + + public final void getCollisions(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List list, boolean loadChunks) { + this.getCollisionsForBlocksOrWorldBorder(entity, axisalignedbb, list, loadChunks, true, false, null); + this.getEntityHardCollisions(entity, axisalignedbb, null, list); + } + + @Override + public boolean getCubes(AxisAlignedBB axisalignedbb) { + return !this.hasAnyCollisions(null, axisalignedbb); + } + + @Override + public boolean getCubes(Entity entity) { + return !this.hasAnyCollisions(entity, entity.getBoundingBox()); + } + + @Override + public boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb) { + if (entity instanceof EntityArmorStand && !entity.world.paperConfig.armorStandEntityLookups) return false; + return !this.hasAnyCollisions(entity, axisalignedbb); + } + + @Override + public boolean getCubes(@Nullable Entity entity, AxisAlignedBB axisalignedbb, Predicate predicate) { + if (entity instanceof EntityArmorStand && !entity.world.paperConfig.armorStandEntityLookups) return false; + return !this.collidesWithAnyBlockOrWorldBorder(entity, axisalignedbb, true, true) && !this.hardCollidesWithAnyEntities(entity, axisalignedbb, predicate); + } + // Tuinity end - optimise collision + // CraftBukkit start @Override protected TileEntity getTileEntity(BlockPosition pos, boolean validate) { @@ -318,6 +661,14 @@ public class WorldServer extends World implements GeneratorAccessSeed { public void doTick(BooleanSupplier booleansupplier) { GameProfilerFiller gameprofilerfiller = this.getMethodProfiler(); + // Tuinity start - optimise checkDespawn + this.playersAffectingSpawning.clear(); + for (EntityPlayer player : this.getPlayers()) { + if (IEntitySelector.affectsSpawning.test(player)) { + this.playersAffectingSpawning.add(player); + } + } + // Tuinity end - optimise checkDespawn this.ticking = true; gameprofilerfiller.enter("world border"); @@ -467,7 +818,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { } timings.scheduledBlocks.stopTiming(); // Paper - this.getMinecraftServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic gameprofilerfiller.exitEnter("raid"); this.timings.raids.startTiming(); // Paper - timings this.persistentRaid.a(); @@ -476,7 +827,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { timings.doSounds.startTiming(); // Spigot this.ak(); timings.doSounds.stopTiming(); // Spigot - this.getMinecraftServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.ticking = false; gameprofilerfiller.exitEnter("entities"); boolean flag3 = true || !this.players.isEmpty() || !this.getForceLoadedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players @@ -492,13 +843,12 @@ public class WorldServer extends World implements GeneratorAccessSeed { } this.tickingEntities = true; - ObjectIterator objectiterator = this.entitiesById.int2ObjectEntrySet().iterator(); + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator objectiterator = this.entitiesForIteration.iterator(); // Tuinity org.spigotmc.ActivationRange.activateEntities(this); // Spigot timings.entityTick.startTiming(); // Spigot while (objectiterator.hasNext()) { - Entry entry = (Entry) objectiterator.next(); - Entity entity = (Entity) entry.getValue(); + Entity entity = (Entity) objectiterator.next(); // Tuinity Entity entity1 = entity.getVehicle(); /* CraftBukkit start - We prevent spawning in general, so this butchering is not needed @@ -514,6 +864,15 @@ public class WorldServer extends World implements GeneratorAccessSeed { gameprofilerfiller.enter("checkDespawn"); if (!entity.dead) { entity.checkDespawn(); + // Tuinity start - optimise notify() + if (entity.inChunk && entity.valid) { + if (this.getChunkProvider().isInEntityTickingChunk(entity)) { + this.updateNavigatorsInRegion(entity); + } + } else { + this.removeNavigatorsFromData(entity); + } + // Tuinity end - optimise notify() } gameprofilerfiller.exit(); @@ -534,14 +893,22 @@ public class WorldServer extends World implements GeneratorAccessSeed { gameprofilerfiller.enter("remove"); if (entity.dead) { this.removeEntityFromChunk(entity); - objectiterator.remove(); + this.entitiesById.remove(entity.getId()); // Tuinity this.unregisterEntity(entity); + } else if (entity.inChunk && entity.valid) { // Tuinity start - optimise notify() + if (this.getChunkProvider().isInEntityTickingChunk(entity)) { + this.updateNavigatorsInRegion(entity); + } + } else { + this.removeNavigatorsFromData(entity); } + // Tuinity end - optimise notify() gameprofilerfiller.exit(); } timings.entityTick.stopTiming(); // Spigot + objectiterator.finishedIterating(); // Tuinity this.tickingEntities = false; // Paper start for (java.lang.Runnable run : this.afterEntityTickingTasks) { @@ -553,7 +920,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { } this.afterEntityTickingTasks.clear(); // Paper end - this.getMinecraftServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic Entity entity2; @@ -563,7 +930,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { } timings.tickEntities.stopTiming(); // Spigot - this.getMinecraftServer().midTickLoadChunks(); // Paper + // Tuinity - replace logic this.tickBlockEntities(); } @@ -809,7 +1176,26 @@ public class WorldServer extends World implements GeneratorAccessSeed { } + // Tuinity start - log detailed entity tick information + static final java.util.concurrent.ConcurrentLinkedDeque currentlyTickingEntities = new java.util.concurrent.ConcurrentLinkedDeque<>(); + + public static List getCurrentlyTickingEntities() { + List ret = Lists.newArrayListWithCapacity(4); + + for (Entity entity : currentlyTickingEntities) { + ret.add(entity); + } + + return ret; + } + // Tuinity end - log detailed entity tick information + public void entityJoinedWorld(Entity entity) { + // Tuinity start - log detailed entity tick information + com.tuinity.tuinity.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); + try { + currentlyTickingEntities.push(entity); + // Tuinity end - log detailed entity tick information if (!(entity instanceof EntityHuman) && !this.getChunkProvider().a(entity)) { this.chunkCheck(entity); } else { @@ -862,6 +1248,11 @@ public class WorldServer extends World implements GeneratorAccessSeed { //} finally { timer.stopTiming(); } // Paper - timings - move up } + // Tuinity start - log detailed entity tick information + } finally { + currentlyTickingEntities.pop(); + } + // Tuinity end - log detailed entity tick information } public void a(Entity entity, Entity entity1) { @@ -920,6 +1311,12 @@ public class WorldServer extends World implements GeneratorAccessSeed { int i = MathHelper.floor(entity.locX() / 16.0D); int j = Math.min(15, Math.max(0, MathHelper.floor(entity.locY() / 16.0D))); // Paper - stay consistent with chunk add/remove behavior int k = MathHelper.floor(entity.locZ() / 16.0D); + // Tuinity start + int oldRegionX = entity.chunkX >> com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.REGION_CHUNK_SIZE_SHIFT; + int oldRegionZ = entity.chunkZ >> com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.REGION_CHUNK_SIZE_SHIFT; + int newRegionX = i >> com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.REGION_CHUNK_SIZE_SHIFT; + int newRegionZ = k >> com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.REGION_CHUNK_SIZE_SHIFT; + // Tuinity end if (!entity.inChunk || entity.chunkX != i || entity.chunkY != j || entity.chunkZ != k) { // Paper start - remove entity if its in a chunk more correctly. @@ -929,6 +1326,12 @@ public class WorldServer extends World implements GeneratorAccessSeed { } // Paper end + // Tuinity start + if (oldRegionX != newRegionX || oldRegionZ != newRegionZ) { + this.removeNavigatorsFromData(entity); + } + // Tuinity end + if (entity.inChunk && this.isChunkLoaded(entity.chunkX, entity.chunkZ)) { this.getChunkAt(entity.chunkX, entity.chunkZ).a(entity, entity.chunkY); } @@ -942,6 +1345,11 @@ public class WorldServer extends World implements GeneratorAccessSeed { } else { this.getChunkAt(i, k).a(entity); } + // Tuinity start + if (entity.inChunk && (oldRegionX != newRegionX || oldRegionZ != newRegionZ)) { + this.addNavigatorsIfPathingToRegion(entity); + } + // Tuinity end } this.getMethodProfiler().exit(); @@ -1297,7 +1705,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { Entity entity = (Entity) iterator.next(); if (!(entity instanceof EntityPlayer)) { - if (this.tickingEntities) { + if (false && this.tickingEntities) { // Tuinity throw (IllegalStateException) SystemUtils.c((Throwable) (new IllegalStateException("Removing entity while ticking!"))); } @@ -1325,6 +1733,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { public void unregisterEntity(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot + this.entitiesForIteration.remove(entity); // Tuinity // Paper start - fix entity registration issues if (entity instanceof EntityComplexPart) { // Usually this is a no-op for complex parts, and ID's should be removed, but go ahead and remove it anyways @@ -1391,17 +1800,108 @@ public class WorldServer extends World implements GeneratorAccessSeed { this.getScoreboard().a(entity); // CraftBukkit start - SPIGOT-5278 if (entity instanceof EntityDrowned) { - this.navigators.remove(((EntityDrowned) entity).navigationWater); - this.navigators.remove(((EntityDrowned) entity).navigationLand); + // Tuinity start + this.navigators.remove(((EntityDrowned) entity).navigationWater); this.navigatorsForIteration.remove(((EntityDrowned) entity).navigationWater); + this.navigators.remove(((EntityDrowned) entity).navigationLand); this.navigatorsForIteration.remove(((EntityDrowned) entity).navigationLand); + // Tuinity end } else // CraftBukkit end if (entity instanceof EntityInsentient) { - this.navigators.remove(((EntityInsentient) entity).getNavigation()); + // Tuinity start + this.navigators.remove(((EntityInsentient) entity).getNavigation()); this.navigatorsForIteration.remove(((EntityInsentient) entity).getNavigation()); + // Tuinity end } new com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent(entity.getBukkitEntity()).callEvent(); // Paper - fire while valid + this.removeNavigatorsFromData(entity); // Tuinity - optimise notify() entity.valid = false; // CraftBukkit } + // Tuinity start - optimise notify() + void removeNavigatorsIfNotPathingFromRegion(Entity entity) { + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ); + if (section != null) { + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators = (com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet)section.getOrCreateData(PlayerChunkMap.RegionData.PATHING_NAVIGATORS); + // Copied from above + if (entity instanceof EntityDrowned) { + if (!((EntityDrowned)entity).navigationWater.isViableForPathRecalculationChecking()) { + navigators.remove(((EntityDrowned)entity).navigationWater); + } + if (!((EntityDrowned)entity).navigationLand.isViableForPathRecalculationChecking()) { + navigators.remove(((EntityDrowned)entity).navigationLand); + } + } else if (entity instanceof EntityInsentient) { + if (!((EntityInsentient)entity).getNavigation().isViableForPathRecalculationChecking()) { + navigators.remove(((EntityInsentient)entity).getNavigation()); + } + } + } + } + + void removeNavigatorsFromData(Entity entity) { + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ); + if (section != null) { + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators = (com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet)section.getOrCreateData(PlayerChunkMap.RegionData.PATHING_NAVIGATORS); + // Copied from above + if (entity instanceof EntityDrowned) { + navigators.remove(((EntityDrowned)entity).navigationWater); + navigators.remove(((EntityDrowned)entity).navigationLand); + } else if (entity instanceof EntityInsentient) { + navigators.remove(((EntityInsentient)entity).getNavigation()); + } + } + } + + void addNavigatorsIfPathingToRegion(Entity entity) { + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ); + if (section != null) { + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators = (com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet)section.getOrCreateData(PlayerChunkMap.RegionData.PATHING_NAVIGATORS); + // Copied from above + if (entity instanceof EntityDrowned) { + if (((EntityDrowned)entity).navigationWater.isViableForPathRecalculationChecking()) { + navigators.add(((EntityDrowned)entity).navigationWater); + } + if (((EntityDrowned)entity).navigationLand.isViableForPathRecalculationChecking()) { + navigators.add(((EntityDrowned)entity).navigationLand); + } + } else if (entity instanceof EntityInsentient) { + if (((EntityInsentient)entity).getNavigation().isViableForPathRecalculationChecking()) { + navigators.add(((EntityInsentient)entity).getNavigation()); + } + } + } + } + + void updateNavigatorsInRegion(Entity entity) { + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = + this.getChunkProvider().playerChunkMap.dataRegionManager.getRegionSection(entity.chunkX, entity.chunkZ); + if (section != null) { + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators = (com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet)section.getOrCreateData(PlayerChunkMap.RegionData.PATHING_NAVIGATORS); + // Copied from above + if (entity instanceof EntityDrowned) { + if (((EntityDrowned)entity).navigationWater.isViableForPathRecalculationChecking()) { + navigators.add(((EntityDrowned)entity).navigationWater); + } else { + navigators.remove(((EntityDrowned)entity).navigationWater); + } + if (((EntityDrowned)entity).navigationLand.isViableForPathRecalculationChecking()) { + navigators.add(((EntityDrowned)entity).navigationLand); + } else { + navigators.remove(((EntityDrowned)entity).navigationLand); + } + } else if (entity instanceof EntityInsentient) { + if (((EntityInsentient)entity).getNavigation().isViableForPathRecalculationChecking()) { + navigators.add(((EntityInsentient)entity).getNavigation()); + } else { + navigators.remove(((EntityInsentient)entity).getNavigation()); + } + } + } + } + // Tuinity end - optimise notify() + private void registerEntity(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot // Paper start - don't double enqueue entity registration @@ -1412,7 +1912,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { return; } // Paper end - if (this.tickingEntities) { + if (false && this.tickingEntities) { // Tuinity if (!entity.isQueuedForRegister) { // Paper this.entitiesToAdd.add(entity); entity.isQueuedForRegister = true; // Paper @@ -1420,6 +1920,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { } else { entity.isQueuedForRegister = false; // Paper this.entitiesById.put(entity.getId(), entity); + this.entitiesForIteration.add(entity); // Tuinity if (entity instanceof EntityEnderDragon) { EntityComplexPart[] aentitycomplexpart = ((EntityEnderDragon) entity).eJ(); int i = aentitycomplexpart.length; @@ -1428,6 +1929,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { EntityComplexPart entitycomplexpart = aentitycomplexpart[j]; this.entitiesById.put(entitycomplexpart.getId(), entitycomplexpart); + this.entitiesForIteration.add(entitycomplexpart); // Tuinity } } @@ -1452,12 +1954,16 @@ public class WorldServer extends World implements GeneratorAccessSeed { // this.getChunkProvider().addEntity(entity); // Paper - moved down below valid=true // CraftBukkit start - SPIGOT-5278 if (entity instanceof EntityDrowned) { - this.navigators.add(((EntityDrowned) entity).navigationWater); - this.navigators.add(((EntityDrowned) entity).navigationLand); + // Tuinity start + this.navigators.add(((EntityDrowned) entity).navigationWater); this.navigatorsForIteration.add(((EntityDrowned) entity).navigationWater); + this.navigators.add(((EntityDrowned) entity).navigationLand); this.navigatorsForIteration.add(((EntityDrowned) entity).navigationLand); + // Tuinity end } else // CraftBukkit end if (entity instanceof EntityInsentient) { - this.navigators.add(((EntityInsentient) entity).getNavigation()); + // Tuinity start + this.navigators.add(((EntityInsentient) entity).getNavigation()); this.navigatorsForIteration.add(((EntityInsentient) entity).getNavigation()); + // Tuinity end } entity.valid = true; // CraftBukkit this.getChunkProvider().addEntity(entity); // Paper - from above to be below valid=true @@ -1473,7 +1979,7 @@ public class WorldServer extends World implements GeneratorAccessSeed { } public void removeEntity(Entity entity) { - if (this.tickingEntities) { + if (false && this.tickingEntities) { // Tuinity throw (IllegalStateException) SystemUtils.c((Throwable) (new IllegalStateException("Removing entity while ticking!"))); } else { this.removeEntityFromChunk(entity); @@ -1569,13 +2075,32 @@ public class WorldServer extends World implements GeneratorAccessSeed { @Override public void notify(BlockPosition blockposition, IBlockData iblockdata, IBlockData iblockdata1, int i) { + org.spigotmc.AsyncCatcher.catchOp("notify call"); // Tuinity this.getChunkProvider().flagDirty(blockposition); VoxelShape voxelshape = iblockdata.getCollisionShape(this, blockposition); VoxelShape voxelshape1 = iblockdata1.getCollisionShape(this, blockposition); if (VoxelShapes.c(voxelshape, voxelshape1, OperatorBoolean.NOT_SAME)) { + // Tuinity start - optimise notify() + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.Region region = this.getChunkProvider().playerChunkMap.dataRegionManager.getRegion(blockposition.getX() >> 4, blockposition.getZ() >> 4); + if (region == null) { + return; + } + // Tuinity end - optimise notify() boolean wasTicking = this.tickingEntities; this.tickingEntities = true; // Paper - Iterator iterator = this.navigators.iterator(); + // Tuinity start + // Tuinity start - optimise notify() + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator> sectionIterator = null; + try { + for (sectionIterator = region.getSections(); sectionIterator.hasNext();) { + com.tuinity.tuinity.chunk.SingleThreadChunkRegionManager.RegionSection section = sectionIterator.next(); + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet navigators = (com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet)section.getData(PlayerChunkMap.RegionData.PATHING_NAVIGATORS); + if (navigators == null) { + continue; + } + com.tuinity.tuinity.util.maplist.IteratorSafeOrderedReferenceSet.Iterator iterator = navigators.iterator(); + // Tuinity end - optimise notify() + try { // Tuinity end while (iterator.hasNext()) { NavigationAbstract navigationabstract = (NavigationAbstract) iterator.next(); @@ -1583,7 +2108,21 @@ public class WorldServer extends World implements GeneratorAccessSeed { if (!navigationabstract.i()) { navigationabstract.b(blockposition); } - } + // Tuinity start - optimise notify() + if (!navigationabstract.isViableForPathRecalculationChecking()) { + navigators.remove(navigationabstract); + } + // Tuinity end - optimise notify() + } + } finally { // Tuinity start + iterator.finishedIterating(); + } // Tuinity end + } // Tuinity start - optimise notify() + } finally { + if (sectionIterator != null) { + sectionIterator.finishedIterating(); + } + } // Tuinity end - optimise notify() this.tickingEntities = wasTicking; // Paper } diff --git a/src/main/java/net/minecraft/server/WorldUpgrader.java b/src/main/java/net/minecraft/server/WorldUpgrader.java index 5ccdc0b87b922724c3dd3085860c55d4959ca0b4..888dae2d5ee8a71e83dd24e5f3c6bc8513016f9d 100644 --- a/src/main/java/net/minecraft/server/WorldUpgrader.java +++ b/src/main/java/net/minecraft/server/WorldUpgrader.java @@ -218,7 +218,7 @@ public class WorldUpgrader { int l = Integer.parseInt(matcher.group(2)) << 5; try { - RegionFile regionfile = new RegionFile(file2, file1, true); + RegionFile regionfile = new RegionFile(file2, file1, true, true); // Tuinity - allow for chunk regionfiles to regen header Throwable throwable = null; try { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java index 7511e38130f38703164395a670f12d1af648ff04..e602efcb3fad390bb6bff1055e782bba909d7694 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftCrashReport.java @@ -37,7 +37,7 @@ public class CraftCrashReport implements CrashReportCallable { value.append("\n Force Loaded Chunks: {"); for (World world : Bukkit.getWorlds()) { value.append(' ').append(world.getName()).append(": {"); - for (Map.Entry> entry : world.getPluginChunkTickets().entrySet()) { + for (Map.Entry> entry : ((CraftWorld)world).getPluginChunkTicketsCoordinates().entrySet()) { // Tuinity - do not load chunks in crash reports value.append(' ').append(entry.getKey().getDescription().getFullName()).append(": ").append(Integer.toString(entry.getValue().size())).append(','); } value.append("},"); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index dc7de2b59ec5ca3e5fba34dbb2aa2e6aed8f95cb..a383ba5e897101a3da1544c877148b43be7a6319 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -232,7 +232,7 @@ import javax.annotation.Nullable; // Paper import javax.annotation.Nonnull; // Paper public final class CraftServer implements Server { - private final String serverName = "Paper"; // Paper + private final String serverName = "Tuinity"; // Paper // Tuinity private final String serverVersion; private final String bukkitVersion = Versioning.getBukkitVersion(); private final Logger logger = Logger.getLogger("Minecraft"); @@ -861,6 +861,7 @@ public final class CraftServer implements Server { org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); // Spigot com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); // Paper + com.tuinity.tuinity.config.TuinityConfig.init((File) console.options.valueOf("tuinity-settings")); // Tuinity - Server Config for (WorldServer world : console.getWorlds()) { world.worldDataServer.setDifficulty(config.difficulty); world.setSpawnFlags(config.spawnMonsters, config.spawnAnimals); @@ -895,6 +896,7 @@ public final class CraftServer implements Server { } world.spigotConfig.init(); // Spigot world.paperConfig.init(); // Paper + world.tuinityConfig.init(); // Tuinity - Server Config } Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper @@ -1858,7 +1860,10 @@ public final class CraftServer implements Server { @Override public boolean isPrimaryThread() { - return Thread.currentThread().equals(console.serverThread) || Thread.currentThread().equals(net.minecraft.server.MinecraftServer.getServer().shutdownThread); // Paper - Fix issues with detecting main thread properly, the only time Watchdog will be used is during a crash shutdown which is a "try our best" scenario + // Tuinity start + final Thread currThread = Thread.currentThread(); + return currThread == console.serverThread || currThread instanceof com.tuinity.tuinity.util.TickThread || currThread.equals(net.minecraft.server.MinecraftServer.getServer().shutdownThread); // Paper - Fix issues with detecting main thread properly, the only time Watchdog will be used is during a crash shutdown which is a "try our best" scenario + // Tuinity End } @Override @@ -2280,6 +2285,14 @@ public final class CraftServer implements Server { return com.destroystokyo.paper.PaperConfig.config; } + // Tuinity start - add config to timings report + @Override + public YamlConfiguration getTuinityConfig() + { + return com.tuinity.tuinity.config.TuinityConfig.config; + } + // Tuinity end - add config to timings report + @Override public void restart() { org.spigotmc.RestartCommand.restart(); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index d86c25593db7cc0a73db1c37af94ae4e41bb4e93..f34e1570052eac83fb3e03b3e361d8d4f451abac 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -341,6 +341,14 @@ public class CraftWorld implements World { this.generator = gen; environment = env; + + //Tuinity start - per world spawn limits + monsterSpawn = world.tuinityConfig.spawnLimitMonsters; + animalSpawn = world.tuinityConfig.spawnLimitAnimals; + waterAmbientSpawn = world.tuinityConfig.spawnLimitWaterAmbient; + waterAnimalSpawn = world.tuinityConfig.spawnLimitWaterAnimals; + ambientSpawn = world.tuinityConfig.spawnLimitAmbient; + //Tuinity end } @Override @@ -414,14 +422,7 @@ public class CraftWorld implements World { @Override public Chunk getChunkAt(int x, int z) { - // Paper start - add ticket to hold chunk for a little while longer if plugin accesses it - net.minecraft.server.Chunk chunk = world.getChunkProvider().getChunkAtIfLoadedImmediately(x, z); - if (chunk == null) { - addTicket(x, z); - chunk = this.world.getChunkProvider().getChunkAt(x, z, true); - } - return chunk.bukkitChunk; - // Paper end + return this.world.getChunkProvider().getChunkAt(x, z, true).bukkitChunk; // Tuinity - revert paper diff } // Paper start @@ -504,6 +505,7 @@ public class CraftWorld implements World { org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot if (isChunkLoaded(x, z)) { world.getChunkProvider().removeTicket(TicketType.PLUGIN, new ChunkCoordIntPair(x, z), 0, Unit.INSTANCE); // Paper + ((ChunkMapDistance)world.getChunkProvider().playerChunkMap.chunkDistanceManager).removeTickets(ChunkCoordIntPair.pair(x, z), TicketType.DELAYED_UNLOAD); // Tuinity - delay chunk unloads - let plugins override } return true; @@ -717,6 +719,30 @@ public class CraftWorld implements World { return ret.entrySet().stream().collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build())); } + // Tuinity start - don't load chunks for crash reports + public Map> getPluginChunkTicketsCoordinates() { + // Copied from above + Map> ret = new HashMap<>(); + ChunkMapDistance chunkDistanceManager = this.world.getChunkProvider().playerChunkMap.chunkDistanceManager; + + for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.tickets.long2ObjectEntrySet()) { + long chunkKey = chunkTickets.getLongKey(); + ArraySetSorted> tickets = chunkTickets.getValue(); + + ChunkCoordIntPair chunk = new ChunkCoordIntPair(chunkKey); + for (Ticket ticket : tickets) { + if (ticket.getTicketType() != TicketType.PLUGIN_TICKET) { + continue; + } + + ret.computeIfAbsent((Plugin) ticket.identifier, (key) -> ImmutableList.builder()).add(chunk); + } + } + + return ret.entrySet().stream().collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build())); + } + // Tuinity end - don't load chunks for crash reports + @Override public boolean isChunkForceLoaded(int x, int z) { return getHandle().getForceLoadedChunks().contains(ChunkCoordIntPair.pair(x, z)); @@ -2562,7 +2588,7 @@ public class CraftWorld implements World { } return this.world.getChunkProvider().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> { net.minecraft.server.Chunk chunk = (net.minecraft.server.Chunk) either.left().orElse(null); - if (chunk != null) addTicket(x, z); // Paper + if (false && chunk != null) addTicket(x, z); // Paper // Tuinity - revert return CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk()); }, MinecraftServer.getServer()); } diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java index 22bde395939f97086e411cef190bb2b1e7ede79a..0f6cb508a170360b6479f9c34048412453fbb89d 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java @@ -138,6 +138,13 @@ public class Main { .defaultsTo(new File("paper.yml")) .describedAs("Yml file"); // Paper end + // Tuinity Start - Server Config + acceptsAll(asList("tuinity", "tuinity-settings"), "File for tuinity settings") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(new File("tuinity.yml")) + .describedAs("Yml file"); + /* Conctete End - Server Config */ // Paper start acceptsAll(asList("server-name"), "Name of the server") @@ -252,7 +259,7 @@ public class Main { if (buildDate.before(deadline.getTime())) { // Paper start - This is some stupid bullshit System.err.println("*** Warning, you've not updated in a while! ***"); - System.err.println("*** Please download a new build as per instructions from https://papermc.io/downloads ***"); // Paper + System.err.println("*** Please download a new build ***"); // Paper // Tuinity //System.err.println("*** Server will start in 20 seconds ***"); //Thread.sleep(TimeUnit.SECONDS.toMillis(20)); // Paper End diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index 475dc1aa2cba77c13033938e719a66707f358914..a6d849facba1526ae2a2b7f3fb9a140d0b50289c 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -508,27 +508,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { entity.setHeadRotation(yaw); } - @Override// Paper start - public java.util.concurrent.CompletableFuture teleportAsync(Location loc, @javax.annotation.Nonnull org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause) { - net.minecraft.server.PlayerChunkMap playerChunkMap = ((CraftWorld) loc.getWorld()).getHandle().getChunkProvider().playerChunkMap; - java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); - - loc.getWorld().getChunkAtAsyncUrgently(loc).thenCompose(chunk -> { - net.minecraft.server.ChunkCoordIntPair pair = new net.minecraft.server.ChunkCoordIntPair(chunk.getX(), chunk.getZ()); - ((CraftWorld) loc.getWorld()).getHandle().getChunkProvider().addTicketAtLevel(net.minecraft.server.TicketType.POST_TELEPORT, pair, 31, 0); - net.minecraft.server.PlayerChunk updatingChunk = playerChunkMap.getUpdatingChunk(pair.pair()); - if (updatingChunk != null) { - return updatingChunk.getEntityTickingFuture(); - } else { - return java.util.concurrent.CompletableFuture.completedFuture(com.mojang.datafixers.util.Either.left(((org.bukkit.craftbukkit.CraftChunk)chunk).getHandle())); - } - }).thenAccept((chunk) -> future.complete(teleport(loc, cause))).exceptionally(ex -> { - future.completeExceptionally(ex); - return null; - }); - return future; - } - // Paper end + // Tuinity @Override public boolean teleport(Location location) { @@ -562,6 +542,37 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { return true; } + // Tuinity start - implement teleportAsync better + @Override + public java.util.concurrent.CompletableFuture teleportAsync(Location location, TeleportCause cause) { + Preconditions.checkArgument(location != null, "location"); + location.checkFinite(); + Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. + + net.minecraft.server.WorldServer world = ((CraftWorld)locationClone.getWorld()).getHandle(); + java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); + + world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), location.getX(), location.getZ(), (list) -> { + net.minecraft.server.ChunkProviderServer chunkProviderServer = world.getChunkProvider(); + for (net.minecraft.server.IChunkAccess chunk : list) { + chunkProviderServer.addTicketAtLevel(net.minecraft.server.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); + } + net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + try { + ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) { + throw (ThreadDeath)throwable; + } + ret.completeExceptionally(throwable); + } + }); + }); + + return ret; + } + // Tuinity end - implement teleportAsync better + @Override public boolean teleport(org.bukkit.entity.Entity destination) { return teleport(destination.getLocation()); diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java index fd32d1450a6a2ede3405be7d31697cd16957f553..c38e514b004a4684026d5a89c606399a4fd7efe1 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java @@ -25,6 +25,10 @@ class CraftAsyncTask extends CraftTask { @Override public void run() { final Thread thread = Thread.currentThread(); + // Tuinity start - name threads according to running plugin + final String nameBefore = thread.getName(); + thread.setName(nameBefore + " - " + this.getOwner().getName()); try { + // Tuinity end - name threads according to running plugin synchronized (workers) { if (getPeriod() == CraftTask.CANCEL) { // Never continue running after cancelled. @@ -92,6 +96,7 @@ class CraftAsyncTask extends CraftTask { } } } + } finally { thread.setName(nameBefore); } // Tuinity - name worker thread according } LinkedList getWorkers() { diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java index 6fa2e271f7f01cd0bf247e2071fa33bd8c5c6cbe..3a9491e9495bec93d5556bd8c09196ea117161d5 100644 --- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java +++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java @@ -113,9 +113,18 @@ public final class CraftScoreboardManager implements ScoreboardManager { // CraftBukkit method public void getScoreboardScores(IScoreboardCriteria criteria, String name, Consumer consumer) { + // Tuinity start - add timings for scoreboard search + // plugins leaking scoreboards will make this very expensive, let server owners debug it easily + co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.startTimingIfSync(); + try { + // Tuinity end - add timings for scoreboard search for (CraftScoreboard scoreboard : scoreboards) { Scoreboard board = scoreboard.board; board.getObjectivesForCriteria(criteria, name, (score) -> consumer.accept(score)); } + } finally { // Tuinity start - add timings for scoreboard search + co.aikar.timings.MinecraftTimings.scoreboardScoreSearch.stopTimingIfSync(); + } + // Tuinity end - add timings for scoreboard search } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java index f72c13bedaa6fa45e26f5dcad564835bdd4af61f..7c0d90552eeb6de7dab174e2ba4acfc89a7b3db0 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java +++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java @@ -35,6 +35,13 @@ public class UnsafeList extends AbstractList implements List, RandomAcc iterPool[0] = new Itr(); } + // Tuinity start + @Override + public void sort(java.util.Comparator c) { + Arrays.sort((E[])this.data, 0, size, c); + } + // Tuinity end + public UnsafeList(int capacity) { this(capacity, 5); } @@ -119,6 +126,32 @@ public class UnsafeList extends AbstractList implements List, RandomAcc return indexOf(o) >= 0; } + // Tuinity start + protected transient int maxSize; + public void setSize(int size) { + if (this.maxSize < this.size) { + this.maxSize = this.size; + } + this.size = size; + } + + public void completeReset() { + if (this.data != null) { + Arrays.fill(this.data, 0, Math.max(this.size, this.maxSize), null); + } + this.size = 0; + this.maxSize = 0; + if (this.iterPool != null) { + for (Iterator temp : this.iterPool) { + if (temp == null) { + continue; + } + ((Itr)temp).valid = false; + } + } + } + // Tuinity end + @Override public void clear() { // Create new array to reset memory usage to initial capacity diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java index 674096cab190d62622f9947853b056f57d43a2a5..001b1e5197eaa51bfff9031aa6c69876c9a47960 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java +++ b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java @@ -11,7 +11,7 @@ public final class Versioning { public static String getBukkitVersion() { String result = "Unknown-Version"; - InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.destroystokyo.paper/paper-api/pom.properties"); + InputStream stream = Bukkit.class.getClassLoader().getResourceAsStream("META-INF/maven/com.tuinity/tuinity-api/pom.properties"); // Tuinity Properties properties = new Properties(); if (stream != null) { diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java index 9f7d2ef932ab41cef5d3d0736d20a7c7e4a2c888..51e9c54cddf4b28ba3d3d892322c487774bdab70 100644 --- a/src/main/java/org/spigotmc/AsyncCatcher.java +++ b/src/main/java/org/spigotmc/AsyncCatcher.java @@ -10,8 +10,9 @@ public class AsyncCatcher public static void catchOp(String reason) { - if ( enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread ) + if ( ( enabled || com.tuinity.tuinity.util.TickThread.STRICT_THREAD_CHECKS ) && !org.bukkit.Bukkit.isPrimaryThread() ) // Tuinity { + MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed thread check for reason: Asynchronous " + reason, new Throwable()); // Tuinity - not all exceptions are printed throw new IllegalStateException( "Asynchronous " + reason + "!" ); } } diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java index ae8903ee1decd22e2ad6138f29fbc757b807e0a7..58d01c6f8abcd9e1792495abd08b186f9d03f834 100644 --- a/src/main/java/org/spigotmc/WatchdogThread.java +++ b/src/main/java/org/spigotmc/WatchdogThread.java @@ -65,6 +65,84 @@ public class WatchdogThread extends Thread } } + // Tuinity start - log detailed tick information + private void dumpTickingInfo() { + Logger log = Bukkit.getServer().getLogger(); + + // ticking entities + for (net.minecraft.server.Entity entity : net.minecraft.server.WorldServer.getCurrentlyTickingEntities()) { + double posX, posY, posZ; + net.minecraft.server.Vec3D mot; + double moveStartX, moveStartY, moveStartZ; + net.minecraft.server.Vec3D moveVec; + synchronized (entity.posLock) { + posX = entity.locX(); + posY = entity.locY(); + posZ = entity.locZ(); + mot = entity.getMot(); + moveStartX = entity.getMoveStartX(); + moveStartY = entity.getMoveStartY(); + moveStartZ = entity.getMoveStartZ(); + moveVec = entity.getMoveVector(); + } + + String entityType = entity.getMinecraftKey().toString(); + java.util.UUID entityUUID = entity.getUniqueID(); + net.minecraft.server.World world = entity.getWorld(); + + log.log(Level.SEVERE, "Ticking entity: " + entityType); + log.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")"); + log.log(Level.SEVERE, "Velocity: " + (mot == null ? "unknown velocity" : mot.toString()) + " (in blocks per tick)"); + if (moveVec != null) { + log.log(Level.SEVERE, "Move call information: "); + log.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")"); + log.log(Level.SEVERE, "Move vector: " + moveVec.toString()); + } + log.log(Level.SEVERE, "UUID: " + entityUUID); + } + + // packet processors + for (net.minecraft.server.PacketListener packetListener : net.minecraft.server.PlayerConnectionUtils.getCurrentPacketProcessors()) { + if (packetListener instanceof net.minecraft.server.PlayerConnection) { + net.minecraft.server.EntityPlayer player = ((net.minecraft.server.PlayerConnection)packetListener).player; + long totalPackets = net.minecraft.server.PlayerConnectionUtils.getTotalProcessedPackets(); + if (player == null) { + log.log(Level.SEVERE, "Handling packet for player connection (null player): " + packetListener); + log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); + } else { + // exclude velocity, this is set client side... Paper will also warn on high velocity set too + double posX, posY, posZ; + double moveStartX, moveStartY, moveStartZ; + net.minecraft.server.Vec3D moveVec; + synchronized (player.posLock) { + posX = player.locX(); + posY = player.locY(); + posZ = player.locZ(); + moveStartX = player.getMoveStartX(); + moveStartY = player.getMoveStartY(); + moveStartZ = player.getMoveStartZ(); + moveVec = player.getMoveVector(); + } + + java.util.UUID entityUUID = player.getUniqueID(); + net.minecraft.server.World world = player.getWorld(); + + log.log(Level.SEVERE, "Handling packet for player '" + player.getName() + "', UUID: " + entityUUID); + log.log(Level.SEVERE, "Position: world: '" + (world == null ? "unknown world?" : world.getWorld().getName()) + "' at location (" + posX + ", " + posY + ", " + posZ + ")"); + if (moveVec != null) { + log.log(Level.SEVERE, "Move call information: "); + log.log(Level.SEVERE, "Start position: (" + moveStartX + ", " + moveStartY + ", " + moveStartZ + ")"); + log.log(Level.SEVERE, "Move vector: " + moveVec.toString()); + } + log.log(Level.SEVERE, "Total packets processed on the main thread for all players: " + totalPackets); + } + } else { + log.log(Level.SEVERE, "Handling packet for connection: " + packetListener); + } + } + } + // Tuinity end - log detailed tick information + @Override public void run() { @@ -121,6 +199,7 @@ public class WatchdogThread extends Thread log.log( Level.SEVERE, "------------------------------" ); log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper + this.dumpTickingInfo(); // Tuinity - log detailed tick information dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( server.serverThread.getId(), Integer.MAX_VALUE ), log ); log.log( Level.SEVERE, "------------------------------" ); //