* Restore paintings and item frames on Folia without teleporting
Rollback spawns a painting or item frame and then teleports it into place, but Folia always throws from Entity#teleport, so paintings are lost and frames never get their recorded facing or item. On Folia the entity is now spawned directly at its recorded block with its facing, art or item set in the pre-spawn consumer. Paper and Spigot are unchanged.
* Spawn Folia hangings through the adapter on 1.19.4 and 1.20.1
Compiling against the current API binds World#spawn(Location, Class,
Consumer) to the java.util.function.Consumer overload, which only exists
from Bukkit 1.20.2. Folia 1.19.4 and 1.20.1 only have the
org.bukkit.util.Consumer overload, so restoring a painting or item frame
there threw NoSuchMethodError. The spawn now goes through
BukkitAdapter.spawn, which calls the java.util.function.Consumer overload
when World has it and otherwise invokes the org.bukkit.util.Consumer
overload by reflection.
* Log armor stand breaks from the retired callback on Folia
When an armor stand dies from non-player damage, the break and its equipment are logged from a task on the stand's scheduler. On Folia the stand is removed in the same tick, so the task is retired instead of run and nothing is logged. The same runnable is now passed as the retired callback, so exactly one of them logs the break.
* Log armor stand breaks from EntityDeathEvent instead of isDead
The damage listeners now only capture the attacker and the stand's
contents, and EntityDeathEvent logs the break when the stand actually
dies. The scheduled task and Folia's retired callback just discard a
capture that was never used, so a stand that survived the hit and was
unloaded or otherwise removed before the callback ran no longer
produces break and container rows, and the retired callback never
touches the world.
* Materialize the MySQL sign text filter as a derived table
MySQL cannot semi-join the rowid IN (UNION ...) subquery the sign text filter builds, so it runs it as a dependent subquery, re-probing all eight branches for every candidate row and never using the line_N prefix indexes. Wrapping the union in a derived table lets MySQL materialize it once through those indexes. Other databases build the same query as before.
* Inline the MySQL sign text filter so restricted lookups stay indexed
The derived table materialized every sign row matching the text across the whole table before the location, time and user restrictions were applied, so a common prefix at one sign location scanned the full table. MySQL now gets the same inline line predicates DuckDB already uses: restricted lookups filter the rows found through the wid, time or user index, and unrestricted lookups still reach the line prefix indexes through an index merge.
* Split the sign filter comment into two sentences
The explosion scan cloned a Location for every block and built five more per block to find attached neighbours, hashing each one on the region thread. It now keys the map by a packed long position and walks the neighbours from an offset table. The same blocks are logged.
Lookup.countLookupRows and LookupRaw.performLookupRaw closed their ResultSet as the last statement of the try, so any error while reading rows left it, and the statement behind it, open until the connection was returned. Both now close it on every path.
populatedChunks had no world in its key, so a chunk generated in one world suppressed water, lava and vine rows at the same chunk coordinates in every other world, and entries were only removed when a later fluid row happened to land there. It is now kept per world and swept after 240 seconds, the longest window BlockPlaceLogger checks.
A tamed animal whose owner has no known name is stored with a null owner, and restoring it throws a NullPointerException at the owner step, which drops the rest of the restore. A null owner name is now skipped like an empty one.
The consumer ran Material.matchMaterial's two regex passes on names they cannot change, and re-encoded the same blockdata strings into ids on every row. Plain enum names now go straight to getMaterial, and stringToByteData caches its results per string, bounded, reset when the id maps reload and skipped on ClickHouse.
The id allocators read the counter, add one and store it without a lock, so two threads that see new names at the same time (region threads on Folia, lookup and API threads everywhere) can give the same id to two different names. Allocation now holds the ConfigHandler monitor reloadAndGetId already uses, with a re-check, and queues the insert after releasing it.
Every dispense copied the dispenser's block entity twice on the region thread (block.getState() in the dispense listeners and inventory.getHolder() in onInventoryInteract), and both copies were only used for their type, location and live inventory. Paper can hand out non-snapshot states for this, so the dispense path now asks for those through a new PaperInterface.getBlockState(Block, boolean), falling back to a normal snapshot on Spigot.
* Added dispensers config option to disable dispenser logging (fixes#727)
* Only suppress dispenser-caused bone meal and ignition
* Only track bone meal applied by a dispenser
* Identify dispenser fireball ignition by its shooter
* Clarify the dispensers config header